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)
|
- [Conway's Game of Life](./examples/conway)
|
||||||
- [Custom Shader](./examples/custom-shader)
|
- [Custom Shader](./examples/custom-shader)
|
||||||
|
- [Dear ImGui example with `winit`](./examples/imgui-winit)
|
||||||
- [Minimal example with SDL2](./examples/minimal-sdl2)
|
- [Minimal example with SDL2](./examples/minimal-sdl2)
|
||||||
- [Minimal example with `winit`](./examples/minimal-winit)
|
- [Minimal example with `winit`](./examples/minimal-winit)
|
||||||
- [Pixel Invaders](./examples/invaders)
|
- [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