use dear_imgui_rs as imgui;
use dear_imgui_rs::{ConfigFlags, DockFlags, Id, TextureId, WindowFlags};
use dear_imgui_wgpu as imgui_wgpu;
use dear_imgui_winit as imgui_winit;
use pollster::block_on;
use std::marker::PhantomData;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use thiserror::Error;
use tracing::{error, info};
use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::window::{Window, WindowId};
pub use wgpu;
#[cfg(feature = "imnodes")]
use dear_imnodes as imnodes;
#[cfg(feature = "implot")]
use dear_implot as implot;
#[cfg(feature = "implot3d")]
use dear_implot3d as implot3d;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum DearAppError {
#[error("event loop error: {0}")]
EventLoop(#[from] winit::error::EventLoopError),
#[error("AppBuilder::run requires an on_frame callback")]
MissingFrameCallback,
#[error("window creation failed: {0}")]
WindowCreation(#[source] winit::error::OsError),
#[error("WGPU surface creation failed: {0}")]
SurfaceCreation(#[source] wgpu::CreateSurfaceError),
#[error("no suitable WGPU adapter found: {0}")]
AdapterUnavailable(#[source] wgpu::RequestAdapterError),
#[error("WGPU device request failed: {0}")]
DeviceRequest(#[source] wgpu::RequestDeviceError),
#[error("WGPU renderer initialization failed: {0}")]
RendererInit(#[source] imgui_wgpu::RendererError),
#[error("WGPU renderer frame preparation failed: {0}")]
FramePrepare(#[source] imgui_wgpu::RendererError),
#[error("WGPU renderer draw failed: {0}")]
Render(#[source] imgui_wgpu::RendererError),
#[error("WGPU surface lost")]
SurfaceLost,
#[error("WGPU surface outdated")]
SurfaceOutdated,
#[error("WGPU surface timeout")]
SurfaceTimeout,
#[error("WGPU surface validation failed while acquiring the next frame")]
SurfaceValidation,
#[error("Generic error: {0}")]
Generic(String),
}
#[derive(Default, Clone, Copy)]
pub struct AddOnsConfig {
pub with_implot: bool,
pub with_imnodes: bool,
pub with_implot3d: bool,
}
impl AddOnsConfig {
pub fn auto() -> Self {
Self {
with_implot: cfg!(feature = "implot"),
with_imnodes: cfg!(feature = "imnodes"),
with_implot3d: cfg!(feature = "implot3d"),
}
}
}
pub struct AddOns<'a> {
#[cfg(feature = "implot")]
pub implot: Option<&'a implot::PlotContext>,
#[cfg(not(feature = "implot"))]
pub implot: Option<()>,
#[cfg(feature = "imnodes")]
pub imnodes: Option<&'a imnodes::Context>,
#[cfg(not(feature = "imnodes"))]
pub imnodes: Option<()>,
#[cfg(feature = "implot3d")]
pub implot3d: Option<&'a implot3d::Plot3DContext>,
#[cfg(not(feature = "implot3d"))]
pub implot3d: Option<()>,
pub docking: DockingApi<'a>,
pub gpu: GpuApi<'a>,
_marker: PhantomData<&'a ()>,
}
pub struct RunnerConfig {
pub window_title: String,
pub window_size: (f64, f64),
pub present_mode: wgpu::PresentMode,
pub clear_color: [f32; 4],
pub wgpu: WgpuConfig,
pub docking: DockingConfig,
pub ini_filename: Option<PathBuf>,
pub restore_previous_geometry: bool,
pub redraw: RedrawMode,
pub io_config_flags: Option<ConfigFlags>,
pub theme: Option<Theme>,
}
impl Default for RunnerConfig {
fn default() -> Self {
Self {
window_title: format!("Dear ImGui App - {}", env!("CARGO_PKG_VERSION")),
window_size: (1280.0, 720.0),
present_mode: wgpu::PresentMode::Fifo,
clear_color: [0.1, 0.2, 0.3, 1.0],
wgpu: WgpuConfig::default(),
docking: DockingConfig::default(),
ini_filename: None,
restore_previous_geometry: true,
redraw: RedrawMode::Poll,
io_config_flags: None,
theme: None,
}
}
}
pub struct WgpuConfig {
pub backends: wgpu::Backends,
pub power_preference: wgpu::PowerPreference,
pub force_fallback_adapter: bool,
pub device_label: Option<String>,
pub required_features: wgpu::Features,
pub required_limits: wgpu::Limits,
pub memory_hints: wgpu::MemoryHints,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
pub enum WgpuPreset {
#[default]
Default,
HighPerformance,
LowPower,
Balanced,
DownlevelCompatible,
SoftwareFallback,
}
impl Default for WgpuConfig {
fn default() -> Self {
Self {
backends: wgpu::Backends::PRIMARY,
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
device_label: None,
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::default(),
}
}
}
impl WgpuConfig {
#[must_use]
pub fn from_preset(preset: WgpuPreset) -> Self {
match preset {
WgpuPreset::Default => Self::default(),
WgpuPreset::HighPerformance => Self {
power_preference: wgpu::PowerPreference::HighPerformance,
memory_hints: wgpu::MemoryHints::Performance,
..Self::default()
},
WgpuPreset::LowPower => Self {
power_preference: wgpu::PowerPreference::LowPower,
memory_hints: wgpu::MemoryHints::MemoryUsage,
..Self::default()
},
WgpuPreset::Balanced => Self {
power_preference: wgpu::PowerPreference::None,
..Self::default()
},
WgpuPreset::DownlevelCompatible => Self {
power_preference: wgpu::PowerPreference::None,
required_limits: wgpu::Limits::downlevel_defaults(),
..Self::default()
},
WgpuPreset::SoftwareFallback => Self {
power_preference: wgpu::PowerPreference::None,
force_fallback_adapter: true,
required_limits: wgpu::Limits::downlevel_defaults(),
..Self::default()
},
}
}
}
pub struct DockingConfig {
pub enable: bool,
pub auto_dockspace: bool,
pub dockspace_flags: DockFlags,
pub host_window_flags: WindowFlags,
pub host_window_name: &'static str,
}
impl Default for DockingConfig {
fn default() -> Self {
Self {
enable: true,
auto_dockspace: true,
dockspace_flags: DockFlags::PASSTHRU_CENTRAL_NODE,
host_window_flags: WindowFlags::NO_TITLE_BAR
| WindowFlags::NO_RESIZE
| WindowFlags::NO_MOVE
| WindowFlags::NO_COLLAPSE
| WindowFlags::NO_BRING_TO_FRONT_ON_FOCUS
| WindowFlags::NO_NAV_FOCUS,
host_window_name: "DockSpaceHost",
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum RedrawMode {
Poll,
Wait,
WaitUntil { fps: f32 },
}
#[derive(Clone, Copy, Debug)]
pub enum Theme {
Dark,
Light,
Classic,
}
fn apply_theme(ctx: &mut imgui::Context, theme: Theme) {
let preset = match theme {
Theme::Dark => imgui::ThemePreset::Dark,
Theme::Light => imgui::ThemePreset::Light,
Theme::Classic => imgui::ThemePreset::Classic,
};
let mut t = imgui::Theme::default();
t.preset = preset;
t.apply_to_context(ctx);
}
pub struct RunnerCallbacks {
pub on_setup: Option<Box<dyn FnMut(&mut imgui::Context)>>,
pub on_style: Option<Box<dyn FnMut(&mut imgui::Context)>>,
pub on_fonts: Option<Box<dyn FnMut(&mut imgui::Context)>>,
pub on_post_init: Option<Box<dyn FnMut(&mut imgui::Context)>>,
pub on_gpu_init: Option<
Box<dyn FnMut(&Arc<Window>, &wgpu::Device, &wgpu::Queue, &wgpu::SurfaceConfiguration)>,
>,
pub on_event:
Option<Box<dyn FnMut(&winit::event::Event<()>, &Arc<Window>, &mut imgui::Context)>>,
pub on_exit: Option<Box<dyn FnMut(&mut imgui::Context)>>,
}
impl Default for RunnerCallbacks {
fn default() -> Self {
Self {
on_setup: None,
on_style: None,
on_fonts: None,
on_post_init: None,
on_gpu_init: None,
on_event: None,
on_exit: None,
}
}
}
pub struct AppBuilder {
cfg: RunnerConfig,
addons: AddOnsConfig,
cbs: RunnerCallbacks,
on_frame: Option<Box<dyn FnMut(&imgui::Ui, &mut AddOns) + 'static>>,
}
impl AppBuilder {
pub fn new() -> Self {
Self {
cfg: RunnerConfig::default(),
addons: AddOnsConfig::default(),
cbs: RunnerCallbacks::default(),
on_frame: None,
}
}
pub fn with_config(mut self, cfg: RunnerConfig) -> Self {
self.cfg = cfg;
self
}
pub fn with_addons(mut self, addons: AddOnsConfig) -> Self {
self.addons = addons;
self
}
pub fn with_theme(mut self, theme: Theme) -> Self {
self.cfg.theme = Some(theme);
self
}
pub fn on_setup<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
self.cbs.on_setup = Some(Box::new(f));
self
}
pub fn on_style<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
self.cbs.on_style = Some(Box::new(f));
self
}
pub fn on_fonts<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
self.cbs.on_fonts = Some(Box::new(f));
self
}
pub fn on_post_init<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
self.cbs.on_post_init = Some(Box::new(f));
self
}
pub fn on_gpu_init<
F: FnMut(&Arc<Window>, &wgpu::Device, &wgpu::Queue, &wgpu::SurfaceConfiguration) + 'static,
>(
mut self,
f: F,
) -> Self {
self.cbs.on_gpu_init = Some(Box::new(f));
self
}
pub fn on_event<
F: FnMut(&winit::event::Event<()>, &Arc<Window>, &mut imgui::Context) + 'static,
>(
mut self,
f: F,
) -> Self {
self.cbs.on_event = Some(Box::new(f));
self
}
pub fn on_frame<F: FnMut(&imgui::Ui, &mut AddOns) + 'static>(mut self, f: F) -> Self {
self.on_frame = Some(Box::new(f));
self
}
pub fn on_exit<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
self.cbs.on_exit = Some(Box::new(f));
self
}
pub fn run(mut self) -> Result<(), DearAppError> {
let frame_fn = self
.on_frame
.take()
.ok_or(DearAppError::MissingFrameCallback)?;
run_with_callbacks(self.cfg, self.addons, self.cbs, frame_fn)
}
}
pub fn run_simple<F>(mut gui: F) -> Result<(), DearAppError>
where
F: FnMut(&imgui::Ui) + 'static,
{
run(
RunnerConfig::default(),
AddOnsConfig::default(),
move |ui, _addons| gui(ui),
)
}
pub fn run<F>(runner: RunnerConfig, addons_cfg: AddOnsConfig, gui: F) -> Result<(), DearAppError>
where
F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
{
run_with_callbacks(runner, addons_cfg, RunnerCallbacks::default(), gui)
}
pub fn run_with_callbacks<F>(
runner: RunnerConfig,
addons_cfg: AddOnsConfig,
cbs: RunnerCallbacks,
gui: F,
) -> Result<(), DearAppError>
where
F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
{
let event_loop = EventLoop::new()?;
match runner.redraw {
RedrawMode::Poll => event_loop.set_control_flow(ControlFlow::Poll),
RedrawMode::Wait => event_loop.set_control_flow(ControlFlow::Wait),
RedrawMode::WaitUntil { fps } => {
let frame = Duration::from_secs_f32(1.0f32 / fps.max(1.0));
event_loop.set_control_flow(ControlFlow::WaitUntil(Instant::now() + frame));
}
}
let mut app = App::new(runner, addons_cfg, cbs, gui);
info!("Starting Dear App event loop");
event_loop.run_app(&mut app)?;
Ok(())
}
struct ImguiState {
context: imgui::Context,
platform: imgui_winit::WinitPlatform,
renderer: imgui_wgpu::WgpuRenderer,
}
pub struct DockingController {
flags: DockFlags,
}
pub struct DockingApi<'a> {
ctrl: &'a mut DockingController,
}
impl<'a> DockingApi<'a> {
pub fn flags(&self) -> DockFlags {
DockFlags::from_bits_retain(self.ctrl.flags.bits())
}
pub fn set_flags(&mut self, flags: DockFlags) {
self.ctrl.flags = flags;
}
}
pub struct GpuApi<'a> {
device: &'a wgpu::Device,
queue: &'a wgpu::Queue,
renderer: &'a mut imgui_wgpu::WgpuRenderer,
}
impl<'a> GpuApi<'a> {
pub fn device(&self) -> &wgpu::Device {
self.device
}
pub fn queue(&self) -> &wgpu::Queue {
self.queue
}
pub fn register_texture(
&mut self,
texture: &wgpu::Texture,
view: &wgpu::TextureView,
) -> TextureId {
self.renderer.register_external_texture(texture, view)
}
pub fn update_texture_view(&mut self, tex_id: TextureId, view: &wgpu::TextureView) -> bool {
self.renderer.update_external_texture_view(tex_id, view)
}
pub fn unregister_texture(&mut self, tex_id: TextureId) {
self.renderer.unregister_texture(tex_id)
}
pub fn update_texture_data(
&mut self,
texture_data: &mut dear_imgui_rs::TextureData,
) -> Result<(), String> {
let res = self
.renderer
.update_texture(texture_data)
.map_err(|e| format!("update_texture failed: {e}"))?;
res.apply_to(texture_data);
Ok(())
}
}
struct AppWindow {
#[allow(dead_code)]
instance: wgpu::Instance,
device: wgpu::Device,
queue: wgpu::Queue,
window: Arc<Window>,
surface_desc: wgpu::SurfaceConfiguration,
surface: wgpu::Surface<'static>,
imgui: ImguiState,
#[cfg(feature = "implot")]
implot_ctx: Option<implot::PlotContext>,
#[cfg(feature = "imnodes")]
imnodes_ctx: Option<imnodes::Context>,
#[cfg(feature = "implot3d")]
implot3d_ctx: Option<implot3d::Plot3DContext>,
clear_color: wgpu::Color,
docking_ctrl: DockingController,
}
impl AppWindow {
fn new(
event_loop: &ActiveEventLoop,
cfg: &RunnerConfig,
addons: &AddOnsConfig,
cbs: &mut RunnerCallbacks,
) -> Result<Self, DearAppError> {
let _ = addons;
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: cfg.wgpu.backends,
..wgpu::InstanceDescriptor::new_without_display_handle()
});
let window = {
let size = LogicalSize::new(cfg.window_size.0, cfg.window_size.1);
Arc::new(
event_loop
.create_window(
Window::default_attributes()
.with_title(cfg.window_title.clone())
.with_inner_size(size),
)
.map_err(DearAppError::WindowCreation)?,
)
};
let surface = instance
.create_surface(window.clone())
.map_err(DearAppError::SurfaceCreation)?;
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: cfg.wgpu.power_preference,
compatible_surface: Some(&surface),
force_fallback_adapter: cfg.wgpu.force_fallback_adapter,
}))
.map_err(DearAppError::AdapterUnavailable)?;
let device_desc = wgpu::DeviceDescriptor {
label: cfg.wgpu.device_label.as_deref(),
required_features: cfg.wgpu.required_features,
required_limits: cfg.wgpu.required_limits.clone(),
memory_hints: cfg.wgpu.memory_hints.clone(),
..Default::default()
};
let (device, queue) =
block_on(adapter.request_device(&device_desc)).map_err(DearAppError::DeviceRequest)?;
let physical_size = window.inner_size();
let caps = surface.get_capabilities(&adapter);
let preferred_srgb = [
wgpu::TextureFormat::Bgra8UnormSrgb,
wgpu::TextureFormat::Rgba8UnormSrgb,
];
let format = preferred_srgb
.iter()
.cloned()
.find(|f| caps.formats.contains(f))
.unwrap_or(caps.formats[0]);
let surface_desc = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: physical_size.width,
height: physical_size.height,
present_mode: cfg.present_mode,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &surface_desc);
if let Some(cb) = cbs.on_gpu_init.as_mut() {
cb(&window, &device, &queue, &surface_desc);
}
let mut context = imgui::Context::create();
if !cfg.restore_previous_geometry {
let _ = context.set_ini_filename(None::<String>);
} else if let Some(p) = &cfg.ini_filename {
let _ = context.set_ini_filename(Some(p.clone()));
} else {
let _ = context.set_ini_filename(None::<String>);
}
if let Some(cb) = cbs.on_setup.as_mut() {
cb(&mut context);
}
if let Some(theme) = cfg.theme {
apply_theme(&mut context, theme);
}
if let Some(cb) = cbs.on_style.as_mut() {
cb(&mut context);
}
if let Some(cb) = cbs.on_fonts.as_mut() {
cb(&mut context);
}
let mut platform = imgui_winit::WinitPlatform::new(&mut context);
platform.attach_window(&window, imgui_winit::HiDpiMode::Default, &mut context);
let init_info =
imgui_wgpu::WgpuInitInfo::new(device.clone(), queue.clone(), surface_desc.format);
let mut renderer = imgui_wgpu::WgpuRenderer::new(init_info, &mut context)
.map_err(DearAppError::RendererInit)?;
renderer.set_gamma_mode(imgui_wgpu::GammaMode::Auto);
{
let io = context.io_mut();
let mut flags = io.config_flags();
if cfg.docking.enable {
flags.insert(ConfigFlags::DOCKING_ENABLE);
}
if let Some(extra) = &cfg.io_config_flags {
let merged = flags.bits() | extra.bits();
flags = ConfigFlags::from_bits_retain(merged);
}
io.set_config_flags(flags);
}
#[cfg(feature = "implot")]
let implot_ctx = if addons.with_implot {
Some(implot::PlotContext::create(&context))
} else {
None
};
#[cfg(feature = "imnodes")]
let imnodes_ctx = if addons.with_imnodes {
Some(imnodes::Context::create(&context))
} else {
None
};
#[cfg(feature = "implot3d")]
let implot3d_ctx = if addons.with_implot3d {
Some(implot3d::Plot3DContext::create(&context))
} else {
None
};
let imgui = ImguiState {
context,
platform,
renderer,
};
Ok(Self {
instance,
device,
queue,
window,
surface_desc,
surface,
imgui,
#[cfg(feature = "implot")]
implot_ctx,
#[cfg(feature = "imnodes")]
imnodes_ctx,
#[cfg(feature = "implot3d")]
implot3d_ctx,
clear_color: wgpu::Color {
r: cfg.clear_color[0] as f64,
g: cfg.clear_color[1] as f64,
b: cfg.clear_color[2] as f64,
a: cfg.clear_color[3] as f64,
},
docking_ctrl: DockingController {
flags: DockFlags::from_bits_retain(cfg.docking.dockspace_flags.bits()),
},
})
}
fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 {
self.surface_desc.width = new_size.width;
self.surface_desc.height = new_size.height;
self.surface.configure(&self.device, &self.surface_desc);
}
}
fn render<F>(&mut self, gui: &mut F, docking: &DockingConfig) -> Result<(), DearAppError>
where
F: FnMut(&imgui::Ui, &mut AddOns),
{
self.imgui
.platform
.prepare_frame(&self.window, &mut self.imgui.context);
let ui = self.imgui.context.frame();
if docking.enable && docking.auto_dockspace {
let viewport = ui.main_viewport();
ui.set_next_window_viewport(viewport.id());
let pos = viewport.pos();
let size = viewport.size();
let current_flags = DockFlags::from_bits_retain(self.docking_ctrl.flags.bits());
let mut win_flags = docking.host_window_flags;
if current_flags.contains(DockFlags::PASSTHRU_CENTRAL_NODE) {
win_flags |= WindowFlags::NO_BACKGROUND;
}
ui.window(docking.host_window_name)
.flags(win_flags)
.position([pos[0], pos[1]], imgui::Condition::Always)
.size([size[0], size[1]], imgui::Condition::Always)
.build(|| {
let ds_flags = DockFlags::from_bits_retain(current_flags.bits());
let _ = ui.dockspace_over_main_viewport_with_flags(Id::from(0u32), ds_flags);
});
}
let mut addons = AddOns {
#[cfg(feature = "implot")]
implot: self.implot_ctx.as_ref(),
#[cfg(not(feature = "implot"))]
implot: None,
#[cfg(feature = "imnodes")]
imnodes: self.imnodes_ctx.as_ref(),
#[cfg(not(feature = "imnodes"))]
imnodes: None,
#[cfg(feature = "implot3d")]
implot3d: self.implot3d_ctx.as_ref(),
#[cfg(not(feature = "implot3d"))]
implot3d: None,
docking: DockingApi {
ctrl: &mut self.docking_ctrl,
},
gpu: GpuApi {
device: &self.device,
queue: &self.queue,
renderer: &mut self.imgui.renderer,
},
_marker: PhantomData,
};
gui(&ui, &mut addons);
self.imgui
.platform
.prepare_render_with_ui(&ui, &self.window);
let draw_data = self.imgui.context.render();
let (frame, reconfigure_after_present) = match self.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(frame) => (frame, false),
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => (frame, true),
wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => {
self.surface.configure(&self.device, &self.surface_desc);
return Ok(());
}
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
return Ok(());
}
wgpu::CurrentSurfaceTexture::Validation => {
return Err(DearAppError::SurfaceValidation);
}
};
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(self.clear_color),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
self.imgui
.renderer
.new_frame()
.map_err(DearAppError::FramePrepare)?;
self.imgui
.renderer
.render_draw_data(draw_data, &mut rpass)
.map_err(DearAppError::Render)?;
}
self.queue.submit(Some(encoder.finish()));
frame.present();
if reconfigure_after_present {
self.surface.configure(&self.device, &self.surface_desc);
}
Ok(())
}
}
struct App<F>
where
F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
{
cfg: RunnerConfig,
addons_cfg: AddOnsConfig,
window: Option<AppWindow>,
gui: F,
cbs: RunnerCallbacks,
last_wake: Instant,
}
impl<F> App<F>
where
F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
{
fn new(cfg: RunnerConfig, addons_cfg: AddOnsConfig, cbs: RunnerCallbacks, gui: F) -> Self {
Self {
cfg,
addons_cfg,
window: None,
gui,
cbs,
last_wake: Instant::now(),
}
}
}
impl<F> ApplicationHandler for App<F>
where
F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
{
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_none() {
match AppWindow::new(event_loop, &self.cfg, &self.addons_cfg, &mut self.cbs) {
Ok(window) => {
self.window = Some(window);
info!("Window created successfully");
if let Some(cb) = self.cbs.on_post_init.as_mut() {
if let Some(w) = self.window.as_mut() {
cb(&mut w.imgui.context);
}
}
if let Some(w) = self.window.as_ref() {
w.window.request_redraw();
}
}
Err(e) => {
error!("Failed to create window: {e}");
event_loop.exit();
}
}
}
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
match event {
WindowEvent::RedrawRequested => {
let mut need_recreate = false;
if let Some(window) = self.window.as_mut() {
let full_event: winit::event::Event<()> = winit::event::Event::WindowEvent {
window_id,
event: event.clone(),
};
if let Some(cb) = self.cbs.on_event.as_mut() {
cb(&full_event, &window.window, &mut window.imgui.context);
}
window.imgui.platform.handle_event(
&mut window.imgui.context,
&window.window,
&full_event,
);
if let Err(e) = window.render(&mut self.gui, &self.cfg.docking) {
error!("Render error: {e}; attempting to recover by recreating GPU state");
need_recreate = true;
} else if matches!(self.cfg.redraw, RedrawMode::Poll) {
window.window.request_redraw();
}
}
if need_recreate {
let mut old_window = self.window.take();
match AppWindow::new(event_loop, &self.cfg, &self.addons_cfg, &mut self.cbs) {
Ok(window) => {
self.window = Some(window);
info!("Successfully recreated window and GPU state after error");
if let Some(window) = self.window.as_mut() {
if let Some(cb) = self.cbs.on_post_init.as_mut() {
cb(&mut window.imgui.context);
}
window.window.request_redraw();
}
}
Err(e) => {
error!("Failed to recreate window after GPU error: {e}");
if let (Some(cb), Some(old)) =
(self.cbs.on_exit.as_mut(), old_window.as_mut())
{
cb(&mut old.imgui.context);
}
event_loop.exit();
}
}
}
}
_ => {
let window = match self.window.as_mut() {
Some(window) => window,
None => return,
};
let full_event: winit::event::Event<()> = winit::event::Event::WindowEvent {
window_id,
event: event.clone(),
};
if let Some(cb) = self.cbs.on_event.as_mut() {
cb(&full_event, &window.window, &mut window.imgui.context);
}
window.imgui.platform.handle_event(
&mut window.imgui.context,
&window.window,
&full_event,
);
match event {
WindowEvent::Resized(physical_size) => {
window.resize(physical_size);
window.window.request_redraw();
}
WindowEvent::ScaleFactorChanged { .. } => {
let new_size = window.window.inner_size();
window.resize(new_size);
window.window.request_redraw();
}
WindowEvent::CloseRequested => {
if let Some(cb) = self.cbs.on_exit.as_mut() {
if let Some(w) = self.window.as_mut() {
cb(&mut w.imgui.context);
}
}
event_loop.exit();
}
_ => {}
}
}
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
match self.cfg.redraw {
RedrawMode::Poll => {
event_loop.set_control_flow(ControlFlow::Poll);
if let Some(window) = &self.window {
window.window.request_redraw();
}
}
RedrawMode::Wait => {
event_loop.set_control_flow(ControlFlow::Wait);
}
RedrawMode::WaitUntil { fps } => {
let frame = Duration::from_secs_f32(1.0f32 / fps.max(1.0));
let now = Instant::now();
let mut next_wake = self.last_wake + frame;
if now >= next_wake {
self.last_wake = now;
next_wake = self.last_wake + frame;
if let Some(window) = &self.window {
window.window.request_redraw();
}
}
event_loop.set_control_flow(ControlFlow::WaitUntil(next_wake));
}
}
}
}
#[cfg(test)]
mod tests {
use super::{AppBuilder, DearAppError};
#[test]
fn app_builder_run_requires_frame_callback_without_starting_event_loop() {
let err = AppBuilder::new()
.run()
.expect_err("builder without on_frame should fail before event loop startup");
assert!(matches!(err, DearAppError::MissingFrameCallback));
assert_eq!(
err.to_string(),
"AppBuilder::run requires an on_frame callback"
);
}
}