diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa905ba..62d5479 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: rust: - stable - beta - - 1.48.0 + - 1.50.0 steps: - name: Checkout sources uses: actions/checkout@v2 @@ -40,7 +40,7 @@ jobs: rust: - stable - beta - - 1.48.0 + - 1.50.0 steps: - name: Checkout sources uses: actions/checkout@v2 diff --git a/README.md b/README.md index a4890ec..2a2e96c 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Rapidly prototype a simple 2D game, pixel-based animations, software renderers, - [Conway's Game of Life](./examples/conway) - [Custom Shader](./examples/custom-shader) - [Dear ImGui example with `winit`](./examples/imgui-winit) +- [Egui example with `winit`](./examples/egui-winit) - [Minimal example with SDL2](./examples/minimal-sdl2) - [Minimal example with `winit`](./examples/minimal-winit) - [Minimal example with `fltk`](./examples/minimal-fltk) diff --git a/examples/egui-winit/Cargo.toml b/examples/egui-winit/Cargo.toml new file mode 100644 index 0000000..3a5fc92 --- /dev/null +++ b/examples/egui-winit/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "egui-winit" +version = "0.1.0" +authors = ["Jay Oster "] +edition = "2018" +publish = false + +[features] +optimize = ["log/release_max_level_warn"] +default = ["optimize"] + +[dependencies] +egui = "0.10" +egui_wgpu_backend = { git = "https://github.com/hasenbanck/egui_wgpu_backend.git", rev = "9d03ad345d15d1e44165849b242d3562fdf3e859" } +egui_winit_platform = { git = "https://github.com/hasenbanck/egui_winit_platform.git", rev = "17298250e9721e8bf2c1d4a17b3e22777f8cb2e8" } +env_logger = "0.7" +log = "0.4" +pixels = { path = "../.." } +winit = "0.24" +winit_input_helper = "0.9" diff --git a/examples/egui-winit/README.md b/examples/egui-winit/README.md new file mode 100644 index 0000000..5b85d66 --- /dev/null +++ b/examples/egui-winit/README.md @@ -0,0 +1,15 @@ +# Egui Example + +![Egui Example](../../img/egui-winit.png) + +Minimal example with `egui` and `winit`. + +## Running + +```bash +cargo run --release --package egui-winit +``` + +## About + +This example is based on `minimal-winit`, and extends it with `egui` to render custom GUI elements over your pixel frame buffer. diff --git a/examples/egui-winit/src/gui.rs b/examples/egui-winit/src/gui.rs new file mode 100644 index 0000000..b95b55b --- /dev/null +++ b/examples/egui-winit/src/gui.rs @@ -0,0 +1,137 @@ +use egui::{ClippedMesh, FontDefinitions}; +use egui_wgpu_backend::{RenderPass, ScreenDescriptor}; +use egui_winit_platform::{Platform, PlatformDescriptor}; +use pixels::{wgpu, PixelsContext}; +use std::time::Instant; + +/// Manages all state required for rendering egui over `Pixels`. +pub(crate) struct Gui { + // State for egui. + start_time: Instant, + platform: Platform, + screen_descriptor: ScreenDescriptor, + rpass: RenderPass, + paint_jobs: Vec, + + // State for the demo app. + window_open: bool, +} + +impl Gui { + /// Create egui. + pub(crate) fn new(width: u32, height: u32, scale_factor: f64, context: &PixelsContext) -> Self { + let platform = Platform::new(PlatformDescriptor { + physical_width: width, + physical_height: height, + scale_factor, + font_definitions: FontDefinitions::default(), + style: Default::default(), + }); + let screen_descriptor = ScreenDescriptor { + physical_width: width, + physical_height: height, + scale_factor: scale_factor as f32, + }; + let rpass = RenderPass::new(&context.device, wgpu::TextureFormat::Bgra8UnormSrgb); + + Self { + start_time: Instant::now(), + platform, + screen_descriptor, + rpass, + paint_jobs: Vec::new(), + window_open: true, + } + } + + /// Handle input events from the window manager. + pub(crate) fn handle_event(&mut self, event: &winit::event::Event<'_, ()>) { + self.platform.handle_event(event); + } + + /// Resize egui. + pub(crate) fn resize(&mut self, width: u32, height: u32) { + self.screen_descriptor.physical_width = width; + self.screen_descriptor.physical_height = height; + } + + /// Update scaling factor. + pub(crate) fn scale_factor(&mut self, scale_factor: f64) { + self.screen_descriptor.scale_factor = scale_factor as f32; + } + + /// Prepare egui. + pub(crate) fn prepare(&mut self) { + self.platform + .update_time(self.start_time.elapsed().as_secs_f64()); + + // Begin the egui frame. + self.platform.begin_frame(); + + // Draw the demo application. + self.ui(&self.platform.context()); + + // End the egui frame and create all paint jobs to prepare for rendering. + let (_output, paint_commands) = self.platform.end_frame(); + self.paint_jobs = self.platform.context().tessellate(paint_commands); + } + + /// Create the UI using egui. + fn ui(&mut self, ctx: &egui::CtxRef) { + egui::TopPanel::top("menubar_container").show(ctx, |ui| { + egui::menu::bar(ui, |ui| { + egui::menu::menu(ui, "File", |ui| { + if ui.button("About...").clicked() { + self.window_open = true; + } + }) + }); + }); + + egui::Window::new("Hello, egui!") + .open(&mut self.window_open) + .show(ctx, |ui| { + ui.label("This example demonstrates using egui with pixels."); + ui.label("Made with 💖 in San Francisco!"); + + ui.separator(); + + ui.horizontal_for_text(egui::TextStyle::Body, |ui| { + ui.label("Learn more about egui at"); + ui.hyperlink("https://docs.rs/egui"); + }); + }); + } + + /// Render egui. + pub(crate) fn render( + &mut self, + encoder: &mut wgpu::CommandEncoder, + render_target: &wgpu::TextureView, + context: &PixelsContext, + ) { + // Upload all resources to the GPU. + self.rpass.update_texture( + &context.device, + &context.queue, + &self.platform.context().texture(), + ); + self.rpass + .update_user_textures(&context.device, &context.queue); + self.rpass.update_buffers( + &context.device, + &context.queue, + &self.paint_jobs, + &self.screen_descriptor, + ); + + // Record all render passes. + self.rpass.execute( + encoder, + render_target, + &self.paint_jobs, + &self.screen_descriptor, + None, + ); + } +} diff --git a/examples/egui-winit/src/main.rs b/examples/egui-winit/src/main.rs new file mode 100644 index 0000000..08251a5 --- /dev/null +++ b/examples/egui-winit/src/main.rs @@ -0,0 +1,160 @@ +#![deny(clippy::all)] +#![forbid(unsafe_code)] + +use crate::gui::Gui; +use log::error; +use pixels::{Error, Pixels, SurfaceTexture}; +use winit::dpi::LogicalSize; +use winit::event::{Event, VirtualKeyCode}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::WindowBuilder; +use winit_input_helper::WinitInputHelper; + +mod gui; + +const WIDTH: u32 = 640; +const HEIGHT: u32 = 480; +const BOX_SIZE: i16 = 64; + +/// Representation of the application state. In this example, a box will bounce around the screen. +struct World { + box_x: i16, + box_y: i16, + velocity_x: i16, + velocity_y: i16, +} + +fn main() -> Result<(), Error> { + env_logger::init(); + let event_loop = EventLoop::new(); + let mut input = WinitInputHelper::new(); + let window = { + let size = LogicalSize::new(WIDTH as f64, HEIGHT as f64); + WindowBuilder::new() + .with_title("Hello Pixels + egui") + .with_inner_size(size) + .with_min_inner_size(size) + .build(&event_loop) + .unwrap() + }; + + let (mut pixels, mut gui) = { + let window_size = window.inner_size(); + let scale_factor = window.scale_factor(); + let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window); + let pixels = Pixels::new(WIDTH, HEIGHT, surface_texture)?; + let gui = Gui::new( + window_size.width, + window_size.height, + scale_factor, + pixels.context(), + ); + + (pixels, gui) + }; + let mut world = World::new(); + + event_loop.run(move |event, _, control_flow| { + // Update egui inputs + gui.handle_event(&event); + + // Draw the current frame + if let Event::RedrawRequested(_) = event { + // Draw the world + world.draw(pixels.get_frame()); + + // Prepare egui + gui.prepare(); + + // Render everything together + let render_result = pixels.render_with(|encoder, render_target, context| { + // Render the world texture + context.scaling_renderer.render(encoder, render_target); + + // Render egui + gui.render(encoder, render_target, context); + }); + + // Basic error handling + if render_result + .map_err(|e| error!("pixels.render() failed: {}", e)) + .is_err() + { + *control_flow = ControlFlow::Exit; + return; + } + } + + // Handle input events + if input.update(&event) { + // Close events + if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { + *control_flow = ControlFlow::Exit; + return; + } + + // Update the scale factor + if let Some(scale_factor) = input.scale_factor() { + gui.scale_factor(scale_factor); + } + + // Resize the window + if let Some(size) = input.window_resized() { + pixels.resize(size.width, size.height); + gui.resize(size.width, size.height); + } + + // Update internal state and request a redraw + world.update(); + window.request_redraw(); + } + }); +} + +impl World { + /// Create a new `World` instance that can draw a moving box. + fn new() -> Self { + Self { + box_x: 24, + box_y: 16, + velocity_x: 1, + velocity_y: 1, + } + } + + /// Update the `World` internal state; bounce the box around the screen. + fn update(&mut self) { + if self.box_x <= 0 || self.box_x + BOX_SIZE > WIDTH as i16 { + self.velocity_x *= -1; + } + if self.box_y <= 0 || self.box_y + BOX_SIZE > HEIGHT as i16 { + self.velocity_y *= -1; + } + + self.box_x += self.velocity_x; + self.box_y += self.velocity_y; + } + + /// Draw the `World` state to the frame buffer. + /// + /// Assumes the default texture format: `wgpu::TextureFormat::Rgba8UnormSrgb` + fn draw(&self, frame: &mut [u8]) { + for (i, pixel) in frame.chunks_exact_mut(4).enumerate() { + let x = (i % WIDTH as usize) as i16; + let y = (i / WIDTH as usize) as i16; + + let inside_the_box = x >= self.box_x + && x < self.box_x + BOX_SIZE + && y >= self.box_y + && y < self.box_y + BOX_SIZE; + + let rgba = if inside_the_box { + [0x5e, 0x48, 0xe8, 0xff] + } else { + [0x48, 0xb2, 0xe8, 0xff] + }; + + pixel.copy_from_slice(&rgba); + } + } +} diff --git a/img/egui-winit.png b/img/egui-winit.png new file mode 100644 index 0000000..93074a6 Binary files /dev/null and b/img/egui-winit.png differ