Add Dear ImGui example (#116)
* Add Dear ImGui example - Closes #90 * Change argument order to match `render_with` * Remove unnecessary borrow * Refactor error messages * Add a space * Refactor Gui field privacy * Add a menu bar and allow the about window to be closed - The local bool is necessary because the menu bar closures are not allowed to borrow `self` for mutable access while `imgui::Ui<'ui>` is alive. - The token-based menu bar lifetime is even more verbose than this.
This commit is contained in:
parent
b6526c27e5
commit
11dca72955
|
@ -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)
|
||||
- [Minimal example with SDL2](./examples/minimal-sdl2)
|
||||
- [Minimal example with `winit`](./examples/minimal-winit)
|
||||
- [Pixel Invaders](./examples/invaders)
|
||||
|
|
20
examples/imgui-winit/Cargo.toml
Normal file
20
examples/imgui-winit/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "imgui-winit"
|
||||
version = "0.1.0"
|
||||
authors = ["Jay Oster <jay@kodewerx.org>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
optimize = ["log/release_max_level_warn"]
|
||||
default = ["optimize"]
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.7"
|
||||
imgui = "0.4"
|
||||
imgui-wgpu = "0.9"
|
||||
imgui-winit-support = { version = "0.4", default-features = false, features = ["winit-22"] }
|
||||
log = "0.4"
|
||||
pixels = { path = "../.." }
|
||||
winit = "0.22"
|
||||
winit_input_helper = "0.6"
|
15
examples/imgui-winit/README.md
Normal file
15
examples/imgui-winit/README.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Dear-ImGui Example
|
||||
|
||||
![Dear-ImGui Example](../../img/imgui-winit.png)
|
||||
|
||||
Minimal example with `imgui` and `winit`.
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
cargo run --release --package imgui-winit
|
||||
```
|
||||
|
||||
## About
|
||||
|
||||
This example is based on `minimal-winit`, and extends it with `imgui` to render custom GUI elements over your pixel frame buffer.
|
150
examples/imgui-winit/src/gui.rs
Normal file
150
examples/imgui-winit/src/gui.rs
Normal file
|
@ -0,0 +1,150 @@
|
|||
use pixels::{raw_window_handle::HasRawWindowHandle, wgpu, PixelsContext};
|
||||
use std::time::Instant;
|
||||
|
||||
/// Manages all state required for rendering Dear ImGui over `Pixels`.
|
||||
pub(crate) struct Gui {
|
||||
imgui: imgui::Context,
|
||||
platform: imgui_winit_support::WinitPlatform,
|
||||
renderer: imgui_wgpu::Renderer,
|
||||
last_frame: Instant,
|
||||
last_cursor: Option<imgui::MouseCursor>,
|
||||
about_open: bool,
|
||||
}
|
||||
|
||||
impl Gui {
|
||||
/// Create Dear ImGui.
|
||||
pub(crate) fn new<W: HasRawWindowHandle>(
|
||||
window: &winit::window::Window,
|
||||
pixels: &pixels::Pixels<W>,
|
||||
) -> Self {
|
||||
// Create Dear ImGui context
|
||||
let mut imgui = imgui::Context::create();
|
||||
imgui.set_ini_filename(None);
|
||||
|
||||
// Initialize winit platform support
|
||||
let mut platform = imgui_winit_support::WinitPlatform::init(&mut imgui);
|
||||
platform.attach_window(
|
||||
imgui.io_mut(),
|
||||
&window,
|
||||
imgui_winit_support::HiDpiMode::Default,
|
||||
);
|
||||
|
||||
// Configure Dear ImGui fonts
|
||||
let hidpi_factor = window.scale_factor();
|
||||
let font_size = (13.0 * hidpi_factor) as f32;
|
||||
imgui.io_mut().font_global_scale = (1.0 / hidpi_factor) as f32;
|
||||
imgui
|
||||
.fonts()
|
||||
.add_font(&[imgui::FontSource::DefaultFontData {
|
||||
config: Some(imgui::FontConfig {
|
||||
oversample_h: 1,
|
||||
pixel_snap_h: true,
|
||||
size_pixels: font_size,
|
||||
..Default::default()
|
||||
}),
|
||||
}]);
|
||||
|
||||
// Fix incorrect colors with sRGB framebuffer
|
||||
let style = imgui.style_mut();
|
||||
for color in 0..style.colors.len() {
|
||||
style.colors[color] = gamma_to_linear(style.colors[color]);
|
||||
}
|
||||
|
||||
// Create Dear ImGui WGPU renderer
|
||||
let device = pixels.device();
|
||||
let queue = pixels.queue();
|
||||
let texture_format = wgpu::TextureFormat::Bgra8UnormSrgb;
|
||||
let renderer = imgui_wgpu::Renderer::new(&mut imgui, &device, &queue, texture_format);
|
||||
|
||||
// Return GUI context
|
||||
Self {
|
||||
imgui,
|
||||
platform,
|
||||
renderer,
|
||||
last_frame: Instant::now(),
|
||||
last_cursor: None,
|
||||
about_open: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare Dear ImGui.
|
||||
pub(crate) fn prepare(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
) -> Result<(), winit::error::ExternalError> {
|
||||
// Prepare Dear ImGui
|
||||
let io = self.imgui.io_mut();
|
||||
self.last_frame = io.update_delta_time(self.last_frame);
|
||||
self.platform.prepare_frame(io, window)
|
||||
}
|
||||
|
||||
/// Render Dear ImGui.
|
||||
pub(crate) fn render(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
render_target: &wgpu::TextureView,
|
||||
context: &PixelsContext,
|
||||
) -> imgui_wgpu::RendererResult<()> {
|
||||
// Start a new Dear ImGui frame and update the cursor
|
||||
let ui = self.imgui.frame();
|
||||
|
||||
let mouse_cursor = ui.mouse_cursor();
|
||||
if self.last_cursor != mouse_cursor {
|
||||
self.last_cursor = mouse_cursor;
|
||||
self.platform.prepare_render(&ui, window);
|
||||
}
|
||||
|
||||
// Draw windows and GUI elements here
|
||||
let mut about_open = false;
|
||||
ui.main_menu_bar(|| {
|
||||
ui.menu(imgui::im_str!("Help"), true, || {
|
||||
about_open = imgui::MenuItem::new(imgui::im_str!("About...")).build(&ui);
|
||||
});
|
||||
});
|
||||
if about_open {
|
||||
self.about_open = true;
|
||||
}
|
||||
|
||||
if self.about_open {
|
||||
ui.show_about_window(&mut self.about_open);
|
||||
}
|
||||
|
||||
// Render Dear ImGui with WGPU
|
||||
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor {
|
||||
attachment: render_target,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: true,
|
||||
},
|
||||
}],
|
||||
depth_stencil_attachment: None,
|
||||
});
|
||||
|
||||
self.renderer
|
||||
.render(ui.render(), &context.queue, &context.device, &mut rpass)
|
||||
}
|
||||
|
||||
/// Handle any outstanding events.
|
||||
pub(crate) fn handle_event(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
event: &winit::event::Event<()>,
|
||||
) {
|
||||
self.platform
|
||||
.handle_event(self.imgui.io_mut(), window, event);
|
||||
}
|
||||
}
|
||||
|
||||
fn gamma_to_linear(color: [f32; 4]) -> [f32; 4] {
|
||||
const GAMMA: f32 = 2.2;
|
||||
|
||||
let x = color[0].powf(GAMMA);
|
||||
let y = color[1].powf(GAMMA);
|
||||
let z = color[2].powf(GAMMA);
|
||||
let w = 1.0 - (1.0 - color[3]).powf(GAMMA);
|
||||
|
||||
[x, y, z, w]
|
||||
}
|
147
examples/imgui-winit/src/main.rs
Normal file
147
examples/imgui-winit/src/main.rs
Normal file
|
@ -0,0 +1,147 @@
|
|||
#![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 + Dear ImGui")
|
||||
.with_inner_size(size)
|
||||
.with_min_inner_size(size)
|
||||
.build(&event_loop)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let mut pixels = {
|
||||
let window_size = window.inner_size();
|
||||
let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window);
|
||||
Pixels::new(WIDTH, HEIGHT, surface_texture)?
|
||||
};
|
||||
let mut world = World::new();
|
||||
|
||||
// Set up Dear ImGui
|
||||
let mut gui = Gui::new(&window, &pixels);
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
// Draw the current frame
|
||||
if let Event::RedrawRequested(_) = event {
|
||||
// Draw the world
|
||||
world.draw(pixels.get_frame());
|
||||
|
||||
// Prepare Dear ImGui
|
||||
gui.prepare(&window).expect("gui.prepare() failed");
|
||||
|
||||
// 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 Dear ImGui
|
||||
gui.render(&window, encoder, render_target, context)
|
||||
.expect("gui.render() failed");
|
||||
});
|
||||
|
||||
// Basic error handling
|
||||
if render_result
|
||||
.map_err(|e| error!("pixels.render() failed: {}", e))
|
||||
.is_err()
|
||||
{
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle input events
|
||||
gui.handle_event(&window, &event);
|
||||
if input.update(event) {
|
||||
// Close events
|
||||
if input.key_pressed(VirtualKeyCode::Escape) || input.quit() {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
|
||||
// Resize the window
|
||||
if let Some(size) = input.window_resized() {
|
||||
pixels.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);
|
||||
}
|
||||
}
|
||||
}
|
BIN
img/imgui-winit.png
Normal file
BIN
img/imgui-winit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Loading…
Reference in a new issue