Skip to content

Commit

Permalink
Add Dear ImGui example (#116)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
parasyte authored Sep 18, 2020
1 parent b6526c2 commit 11dca72
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions examples/imgui-winit/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 changes: 15 additions & 0 deletions examples/imgui-winit/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 150 additions & 0 deletions examples/imgui-winit/src/gui.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 147 additions & 0 deletions examples/imgui-winit/src/main.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Binary file added img/imgui-winit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 11dca72

Please sign in to comment.