1use dear_imgui_rs as imgui;
22use dear_imgui_rs::{ConfigFlags, DockFlags, Id, TextureId, WindowFlags};
23use dear_imgui_wgpu as imgui_wgpu;
24use dear_imgui_winit as imgui_winit;
25use pollster::block_on;
26use std::marker::PhantomData;
27use std::path::PathBuf;
28use std::sync::Arc;
29use std::time::{Duration, Instant};
30use thiserror::Error;
31use tracing::{error, info};
32use winit::application::ApplicationHandler;
33use winit::dpi::LogicalSize;
34use winit::event::WindowEvent;
35use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
36use winit::window::{Window, WindowId};
37
38pub use wgpu;
40
41#[cfg(feature = "imnodes")]
42use dear_imnodes as imnodes;
43#[cfg(feature = "implot")]
44use dear_implot as implot;
45#[cfg(feature = "implot3d")]
46use dear_implot3d as implot3d;
47
48#[derive(Debug, Error)]
49#[non_exhaustive]
50pub enum DearAppError {
51 #[error("event loop error: {0}")]
52 EventLoop(#[from] winit::error::EventLoopError),
53 #[error("AppBuilder::run requires an on_frame callback")]
54 MissingFrameCallback,
55 #[error("window creation failed: {0}")]
56 WindowCreation(#[source] winit::error::OsError),
57 #[error("WGPU surface creation failed: {0}")]
58 SurfaceCreation(#[source] wgpu::CreateSurfaceError),
59 #[error("no suitable WGPU adapter found: {0}")]
60 AdapterUnavailable(#[source] wgpu::RequestAdapterError),
61 #[error("WGPU device request failed: {0}")]
62 DeviceRequest(#[source] wgpu::RequestDeviceError),
63 #[error("WGPU renderer initialization failed: {0}")]
64 RendererInit(#[source] imgui_wgpu::RendererError),
65 #[error("WGPU renderer frame preparation failed: {0}")]
66 FramePrepare(#[source] imgui_wgpu::RendererError),
67 #[error("WGPU renderer draw failed: {0}")]
68 Render(#[source] imgui_wgpu::RendererError),
69 #[error("WGPU surface lost")]
70 SurfaceLost,
71 #[error("WGPU surface outdated")]
72 SurfaceOutdated,
73 #[error("WGPU surface timeout")]
74 SurfaceTimeout,
75 #[error("WGPU surface validation failed while acquiring the next frame")]
76 SurfaceValidation,
77 #[error("Generic error: {0}")]
78 Generic(String),
79}
80
81#[derive(Default, Clone, Copy)]
83pub struct AddOnsConfig {
84 pub with_implot: bool,
85 pub with_imnodes: bool,
86 pub with_implot3d: bool,
87}
88
89impl AddOnsConfig {
90 pub fn auto() -> Self {
94 Self {
95 with_implot: cfg!(feature = "implot"),
96 with_imnodes: cfg!(feature = "imnodes"),
97 with_implot3d: cfg!(feature = "implot3d"),
98 }
99 }
100}
101
102pub struct AddOns<'a> {
104 #[cfg(feature = "implot")]
105 pub implot: Option<&'a implot::PlotContext>,
106 #[cfg(not(feature = "implot"))]
107 pub implot: Option<()>,
108
109 #[cfg(feature = "imnodes")]
110 pub imnodes: Option<&'a imnodes::Context>,
111 #[cfg(not(feature = "imnodes"))]
112 pub imnodes: Option<()>,
113
114 #[cfg(feature = "implot3d")]
115 pub implot3d: Option<&'a implot3d::Plot3DContext>,
116 #[cfg(not(feature = "implot3d"))]
117 pub implot3d: Option<()>,
118 pub docking: DockingApi<'a>,
119 pub gpu: GpuApi<'a>,
120 _marker: PhantomData<&'a ()>,
121}
122
123pub struct RunnerConfig {
125 pub window_title: String,
126 pub window_size: (f64, f64),
127 pub present_mode: wgpu::PresentMode,
128 pub clear_color: [f32; 4],
129 pub wgpu: WgpuConfig,
131 pub docking: DockingConfig,
132 pub ini_filename: Option<PathBuf>,
133 pub restore_previous_geometry: bool,
134 pub redraw: RedrawMode,
135 pub io_config_flags: Option<ConfigFlags>,
138 pub theme: Option<Theme>,
140}
141
142impl Default for RunnerConfig {
143 fn default() -> Self {
144 Self {
145 window_title: format!("Dear ImGui App - {}", env!("CARGO_PKG_VERSION")),
146 window_size: (1280.0, 720.0),
147 present_mode: wgpu::PresentMode::Fifo,
148 clear_color: [0.1, 0.2, 0.3, 1.0],
149 wgpu: WgpuConfig::default(),
150 docking: DockingConfig::default(),
151 ini_filename: None,
152 restore_previous_geometry: true,
153 redraw: RedrawMode::Poll,
154 io_config_flags: None,
155 theme: None,
156 }
157 }
158}
159
160pub struct WgpuConfig {
165 pub backends: wgpu::Backends,
167 pub power_preference: wgpu::PowerPreference,
169 pub force_fallback_adapter: bool,
171 pub device_label: Option<String>,
173 pub required_features: wgpu::Features,
175 pub required_limits: wgpu::Limits,
177 pub memory_hints: wgpu::MemoryHints,
179}
180
181#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
183pub enum WgpuPreset {
184 #[default]
186 Default,
187 HighPerformance,
189 LowPower,
191 Balanced,
193 DownlevelCompatible,
195 SoftwareFallback,
197}
198
199impl Default for WgpuConfig {
200 fn default() -> Self {
201 Self {
202 backends: wgpu::Backends::PRIMARY,
203 power_preference: wgpu::PowerPreference::HighPerformance,
204 force_fallback_adapter: false,
205 device_label: None,
206 required_features: wgpu::Features::empty(),
207 required_limits: wgpu::Limits::default(),
208 memory_hints: wgpu::MemoryHints::default(),
209 }
210 }
211}
212
213impl WgpuConfig {
214 #[must_use]
216 pub fn from_preset(preset: WgpuPreset) -> Self {
217 match preset {
218 WgpuPreset::Default => Self::default(),
219 WgpuPreset::HighPerformance => Self {
220 power_preference: wgpu::PowerPreference::HighPerformance,
221 memory_hints: wgpu::MemoryHints::Performance,
222 ..Self::default()
223 },
224 WgpuPreset::LowPower => Self {
225 power_preference: wgpu::PowerPreference::LowPower,
226 memory_hints: wgpu::MemoryHints::MemoryUsage,
227 ..Self::default()
228 },
229 WgpuPreset::Balanced => Self {
230 power_preference: wgpu::PowerPreference::None,
231 ..Self::default()
232 },
233 WgpuPreset::DownlevelCompatible => Self {
234 power_preference: wgpu::PowerPreference::None,
235 required_limits: wgpu::Limits::downlevel_defaults(),
236 ..Self::default()
237 },
238 WgpuPreset::SoftwareFallback => Self {
239 power_preference: wgpu::PowerPreference::None,
240 force_fallback_adapter: true,
241 required_limits: wgpu::Limits::downlevel_defaults(),
242 ..Self::default()
243 },
244 }
245 }
246}
247
248pub struct DockingConfig {
250 pub enable: bool,
252 pub auto_dockspace: bool,
254 pub dockspace_flags: DockFlags,
256 pub host_window_flags: WindowFlags,
258 pub host_window_name: &'static str,
260}
261
262impl Default for DockingConfig {
263 fn default() -> Self {
264 Self {
265 enable: true,
266 auto_dockspace: true,
267 dockspace_flags: DockFlags::PASSTHRU_CENTRAL_NODE,
268 host_window_flags: WindowFlags::NO_TITLE_BAR
269 | WindowFlags::NO_RESIZE
270 | WindowFlags::NO_MOVE
271 | WindowFlags::NO_COLLAPSE
272 | WindowFlags::NO_BRING_TO_FRONT_ON_FOCUS
273 | WindowFlags::NO_NAV_FOCUS,
274 host_window_name: "DockSpaceHost",
275 }
276 }
277}
278
279#[derive(Clone, Copy, Debug)]
281pub enum RedrawMode {
282 Poll,
284 Wait,
286 WaitUntil { fps: f32 },
288}
289
290#[derive(Clone, Copy, Debug)]
292pub enum Theme {
293 Dark,
294 Light,
295 Classic,
296}
297
298fn apply_theme(ctx: &mut imgui::Context, theme: Theme) {
299 let preset = match theme {
300 Theme::Dark => imgui::ThemePreset::Dark,
301 Theme::Light => imgui::ThemePreset::Light,
302 Theme::Classic => imgui::ThemePreset::Classic,
303 };
304 let mut t = imgui::Theme::default();
305 t.preset = preset;
306 t.apply_to_context(ctx);
307}
308
309pub struct RunnerCallbacks {
311 pub on_setup: Option<Box<dyn FnMut(&mut imgui::Context)>>,
312 pub on_style: Option<Box<dyn FnMut(&mut imgui::Context)>>,
313 pub on_fonts: Option<Box<dyn FnMut(&mut imgui::Context)>>,
314 pub on_post_init: Option<Box<dyn FnMut(&mut imgui::Context)>>,
315 pub on_gpu_init: Option<
316 Box<dyn FnMut(&Arc<Window>, &wgpu::Device, &wgpu::Queue, &wgpu::SurfaceConfiguration)>,
317 >,
318 pub on_event:
319 Option<Box<dyn FnMut(&winit::event::Event<()>, &Arc<Window>, &mut imgui::Context)>>,
320 pub on_exit: Option<Box<dyn FnMut(&mut imgui::Context)>>,
321}
322
323impl Default for RunnerCallbacks {
324 fn default() -> Self {
325 Self {
326 on_setup: None,
327 on_style: None,
328 on_fonts: None,
329 on_post_init: None,
330 on_gpu_init: None,
331 on_event: None,
332 on_exit: None,
333 }
334 }
335}
336
337pub struct AppBuilder {
339 cfg: RunnerConfig,
340 addons: AddOnsConfig,
341 cbs: RunnerCallbacks,
342 on_frame: Option<Box<dyn FnMut(&imgui::Ui, &mut AddOns) + 'static>>,
343}
344
345impl AppBuilder {
346 pub fn new() -> Self {
347 Self {
348 cfg: RunnerConfig::default(),
349 addons: AddOnsConfig::default(),
350 cbs: RunnerCallbacks::default(),
351 on_frame: None,
352 }
353 }
354 pub fn with_config(mut self, cfg: RunnerConfig) -> Self {
355 self.cfg = cfg;
356 self
357 }
358 pub fn with_addons(mut self, addons: AddOnsConfig) -> Self {
359 self.addons = addons;
360 self
361 }
362 pub fn with_theme(mut self, theme: Theme) -> Self {
363 self.cfg.theme = Some(theme);
364 self
365 }
366 pub fn on_setup<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
367 self.cbs.on_setup = Some(Box::new(f));
368 self
369 }
370 pub fn on_style<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
371 self.cbs.on_style = Some(Box::new(f));
372 self
373 }
374 pub fn on_fonts<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
375 self.cbs.on_fonts = Some(Box::new(f));
376 self
377 }
378 pub fn on_post_init<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
379 self.cbs.on_post_init = Some(Box::new(f));
380 self
381 }
382 pub fn on_gpu_init<
383 F: FnMut(&Arc<Window>, &wgpu::Device, &wgpu::Queue, &wgpu::SurfaceConfiguration) + 'static,
384 >(
385 mut self,
386 f: F,
387 ) -> Self {
388 self.cbs.on_gpu_init = Some(Box::new(f));
389 self
390 }
391 pub fn on_event<
392 F: FnMut(&winit::event::Event<()>, &Arc<Window>, &mut imgui::Context) + 'static,
393 >(
394 mut self,
395 f: F,
396 ) -> Self {
397 self.cbs.on_event = Some(Box::new(f));
398 self
399 }
400 pub fn on_frame<F: FnMut(&imgui::Ui, &mut AddOns) + 'static>(mut self, f: F) -> Self {
401 self.on_frame = Some(Box::new(f));
402 self
403 }
404 pub fn on_exit<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
405 self.cbs.on_exit = Some(Box::new(f));
406 self
407 }
408 pub fn run(mut self) -> Result<(), DearAppError> {
409 let frame_fn = self
410 .on_frame
411 .take()
412 .ok_or(DearAppError::MissingFrameCallback)?;
413 run_with_callbacks(self.cfg, self.addons, self.cbs, frame_fn)
414 }
415}
416
417pub fn run_simple<F>(mut gui: F) -> Result<(), DearAppError>
423where
424 F: FnMut(&imgui::Ui) + 'static,
425{
426 run(
427 RunnerConfig::default(),
428 AddOnsConfig::default(),
429 move |ui, _addons| gui(ui),
430 )
431}
432
433pub fn run<F>(runner: RunnerConfig, addons_cfg: AddOnsConfig, gui: F) -> Result<(), DearAppError>
437where
438 F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
439{
440 run_with_callbacks(runner, addons_cfg, RunnerCallbacks::default(), gui)
441}
442
443pub fn run_with_callbacks<F>(
445 runner: RunnerConfig,
446 addons_cfg: AddOnsConfig,
447 cbs: RunnerCallbacks,
448 gui: F,
449) -> Result<(), DearAppError>
450where
451 F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
452{
453 let event_loop = EventLoop::new()?;
454 match runner.redraw {
455 RedrawMode::Poll => event_loop.set_control_flow(ControlFlow::Poll),
456 RedrawMode::Wait => event_loop.set_control_flow(ControlFlow::Wait),
457 RedrawMode::WaitUntil { fps } => {
458 let frame = Duration::from_secs_f32(1.0f32 / fps.max(1.0));
459 event_loop.set_control_flow(ControlFlow::WaitUntil(Instant::now() + frame));
460 }
461 }
462
463 let mut app = App::new(runner, addons_cfg, cbs, gui);
464 info!("Starting Dear App event loop");
465 event_loop.run_app(&mut app)?;
466 Ok(())
467}
468
469struct ImguiState {
470 context: imgui::Context,
471 platform: imgui_winit::WinitPlatform,
472 renderer: imgui_wgpu::WgpuRenderer,
473}
474
475pub struct DockingController {
477 flags: DockFlags,
478}
479
480pub struct DockingApi<'a> {
481 ctrl: &'a mut DockingController,
482}
483
484impl<'a> DockingApi<'a> {
485 pub fn flags(&self) -> DockFlags {
486 DockFlags::from_bits_retain(self.ctrl.flags.bits())
487 }
488 pub fn set_flags(&mut self, flags: DockFlags) {
489 self.ctrl.flags = flags;
490 }
491}
492
493pub struct GpuApi<'a> {
496 device: &'a wgpu::Device,
497 queue: &'a wgpu::Queue,
498 renderer: &'a mut imgui_wgpu::WgpuRenderer,
499}
500
501impl<'a> GpuApi<'a> {
502 pub fn device(&self) -> &wgpu::Device {
504 self.device
505 }
506 pub fn queue(&self) -> &wgpu::Queue {
508 self.queue
509 }
510 pub fn register_texture(
512 &mut self,
513 texture: &wgpu::Texture,
514 view: &wgpu::TextureView,
515 ) -> TextureId {
516 self.renderer.register_external_texture(texture, view)
517 }
518 pub fn update_texture_view(&mut self, tex_id: TextureId, view: &wgpu::TextureView) -> bool {
520 self.renderer.update_external_texture_view(tex_id, view)
521 }
522 pub fn unregister_texture(&mut self, tex_id: TextureId) {
524 self.renderer.unregister_texture(tex_id)
525 }
526 pub fn update_texture_data(
528 &mut self,
529 texture_data: &mut dear_imgui_rs::TextureData,
530 ) -> Result<(), String> {
531 let res = self
532 .renderer
533 .update_texture(texture_data)
534 .map_err(|e| format!("update_texture failed: {e}"))?;
535 res.apply_to(texture_data);
537 Ok(())
538 }
539}
540
541struct AppWindow {
542 #[allow(dead_code)]
544 instance: wgpu::Instance,
545 device: wgpu::Device,
546 queue: wgpu::Queue,
547 window: Arc<Window>,
548 surface_desc: wgpu::SurfaceConfiguration,
549 surface: wgpu::Surface<'static>,
550 imgui: ImguiState,
551
552 #[cfg(feature = "implot")]
554 implot_ctx: Option<implot::PlotContext>,
555 #[cfg(feature = "imnodes")]
556 imnodes_ctx: Option<imnodes::Context>,
557 #[cfg(feature = "implot3d")]
558 implot3d_ctx: Option<implot3d::Plot3DContext>,
559
560 clear_color: wgpu::Color,
562 docking_ctrl: DockingController,
563}
564
565impl AppWindow {
566 fn new(
567 event_loop: &ActiveEventLoop,
568 cfg: &RunnerConfig,
569 addons: &AddOnsConfig,
570 cbs: &mut RunnerCallbacks,
571 ) -> Result<Self, DearAppError> {
572 let _ = addons;
573
574 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
576 backends: cfg.wgpu.backends,
577 ..wgpu::InstanceDescriptor::new_without_display_handle()
578 });
579
580 let window = {
581 let size = LogicalSize::new(cfg.window_size.0, cfg.window_size.1);
582 Arc::new(
583 event_loop
584 .create_window(
585 Window::default_attributes()
586 .with_title(cfg.window_title.clone())
587 .with_inner_size(size),
588 )
589 .map_err(DearAppError::WindowCreation)?,
590 )
591 };
592
593 let surface = instance
594 .create_surface(window.clone())
595 .map_err(DearAppError::SurfaceCreation)?;
596
597 let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
598 power_preference: cfg.wgpu.power_preference,
599 compatible_surface: Some(&surface),
600 force_fallback_adapter: cfg.wgpu.force_fallback_adapter,
601 }))
602 .map_err(DearAppError::AdapterUnavailable)?;
603
604 let device_desc = wgpu::DeviceDescriptor {
605 label: cfg.wgpu.device_label.as_deref(),
606 required_features: cfg.wgpu.required_features,
607 required_limits: cfg.wgpu.required_limits.clone(),
608 memory_hints: cfg.wgpu.memory_hints.clone(),
609 ..Default::default()
610 };
611 let (device, queue) =
612 block_on(adapter.request_device(&device_desc)).map_err(DearAppError::DeviceRequest)?;
613
614 let physical_size = window.inner_size();
616 let caps = surface.get_capabilities(&adapter);
617 let preferred_srgb = [
618 wgpu::TextureFormat::Bgra8UnormSrgb,
619 wgpu::TextureFormat::Rgba8UnormSrgb,
620 ];
621 let format = preferred_srgb
622 .iter()
623 .cloned()
624 .find(|f| caps.formats.contains(f))
625 .unwrap_or(caps.formats[0]);
626
627 let surface_desc = wgpu::SurfaceConfiguration {
628 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
629 format,
630 width: physical_size.width,
631 height: physical_size.height,
632 present_mode: cfg.present_mode,
633 alpha_mode: wgpu::CompositeAlphaMode::Auto,
634 view_formats: vec![],
635 desired_maximum_frame_latency: 2,
636 };
637
638 surface.configure(&device, &surface_desc);
639
640 if let Some(cb) = cbs.on_gpu_init.as_mut() {
641 cb(&window, &device, &queue, &surface_desc);
642 }
643
644 let mut context = imgui::Context::create();
646 if !cfg.restore_previous_geometry {
648 let _ = context.set_ini_filename(None::<String>);
649 } else if let Some(p) = &cfg.ini_filename {
650 let _ = context.set_ini_filename(Some(p.clone()));
651 } else {
652 let _ = context.set_ini_filename(None::<String>);
653 }
654
655 if let Some(cb) = cbs.on_setup.as_mut() {
657 cb(&mut context);
658 }
659 if let Some(theme) = cfg.theme {
661 apply_theme(&mut context, theme);
662 }
663 if let Some(cb) = cbs.on_style.as_mut() {
664 cb(&mut context);
665 }
666 if let Some(cb) = cbs.on_fonts.as_mut() {
667 cb(&mut context);
668 }
669
670 let mut platform = imgui_winit::WinitPlatform::new(&mut context);
671 platform.attach_window(&window, imgui_winit::HiDpiMode::Default, &mut context);
672
673 let init_info =
674 imgui_wgpu::WgpuInitInfo::new(device.clone(), queue.clone(), surface_desc.format);
675 let mut renderer = imgui_wgpu::WgpuRenderer::new(init_info, &mut context)
676 .map_err(DearAppError::RendererInit)?;
677 renderer.set_gamma_mode(imgui_wgpu::GammaMode::Auto);
678
679 {
681 let io = context.io_mut();
682 let mut flags = io.config_flags();
683 if cfg.docking.enable {
684 flags.insert(ConfigFlags::DOCKING_ENABLE);
685 }
686 if let Some(extra) = &cfg.io_config_flags {
687 let merged = flags.bits() | extra.bits();
688 flags = ConfigFlags::from_bits_retain(merged);
689 }
690 io.set_config_flags(flags);
691 }
692
693 #[cfg(feature = "implot")]
694 let implot_ctx = if addons.with_implot {
695 Some(implot::PlotContext::create(&context))
696 } else {
697 None
698 };
699
700 #[cfg(feature = "imnodes")]
701 let imnodes_ctx = if addons.with_imnodes {
702 Some(imnodes::Context::create(&context))
703 } else {
704 None
705 };
706
707 #[cfg(feature = "implot3d")]
708 let implot3d_ctx = if addons.with_implot3d {
709 Some(implot3d::Plot3DContext::create(&context))
710 } else {
711 None
712 };
713
714 let imgui = ImguiState {
715 context,
716 platform,
717 renderer,
718 };
719
720 Ok(Self {
721 instance,
722 device,
723 queue,
724 window,
725 surface_desc,
726 surface,
727 imgui,
728 #[cfg(feature = "implot")]
729 implot_ctx,
730 #[cfg(feature = "imnodes")]
731 imnodes_ctx,
732 #[cfg(feature = "implot3d")]
733 implot3d_ctx,
734 clear_color: wgpu::Color {
735 r: cfg.clear_color[0] as f64,
736 g: cfg.clear_color[1] as f64,
737 b: cfg.clear_color[2] as f64,
738 a: cfg.clear_color[3] as f64,
739 },
740 docking_ctrl: DockingController {
741 flags: DockFlags::from_bits_retain(cfg.docking.dockspace_flags.bits()),
742 },
743 })
744 }
745
746 fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
747 if new_size.width > 0 && new_size.height > 0 {
748 self.surface_desc.width = new_size.width;
749 self.surface_desc.height = new_size.height;
750 self.surface.configure(&self.device, &self.surface_desc);
751 }
752 }
753
754 fn render<F>(&mut self, gui: &mut F, docking: &DockingConfig) -> Result<(), DearAppError>
755 where
756 F: FnMut(&imgui::Ui, &mut AddOns),
757 {
758 self.imgui
759 .platform
760 .prepare_frame(&self.window, &mut self.imgui.context);
761 let ui = self.imgui.context.frame();
762
763 if docking.enable && docking.auto_dockspace {
765 let viewport = ui.main_viewport();
766 ui.set_next_window_viewport(viewport.id());
768 let pos = viewport.pos();
769 let size = viewport.size();
770 let current_flags = DockFlags::from_bits_retain(self.docking_ctrl.flags.bits());
772 let mut win_flags = docking.host_window_flags;
773 if current_flags.contains(DockFlags::PASSTHRU_CENTRAL_NODE) {
774 win_flags |= WindowFlags::NO_BACKGROUND;
775 }
776 ui.window(docking.host_window_name)
777 .flags(win_flags)
778 .position([pos[0], pos[1]], imgui::Condition::Always)
779 .size([size[0], size[1]], imgui::Condition::Always)
780 .build(|| {
781 let ds_flags = DockFlags::from_bits_retain(current_flags.bits());
782 let _ = ui.dockspace_over_main_viewport_with_flags(Id::from(0u32), ds_flags);
783 });
784 }
785
786 let mut addons = AddOns {
788 #[cfg(feature = "implot")]
789 implot: self.implot_ctx.as_ref(),
790 #[cfg(not(feature = "implot"))]
791 implot: None,
792 #[cfg(feature = "imnodes")]
793 imnodes: self.imnodes_ctx.as_ref(),
794 #[cfg(not(feature = "imnodes"))]
795 imnodes: None,
796 #[cfg(feature = "implot3d")]
797 implot3d: self.implot3d_ctx.as_ref(),
798 #[cfg(not(feature = "implot3d"))]
799 implot3d: None,
800 docking: DockingApi {
801 ctrl: &mut self.docking_ctrl,
802 },
803 gpu: GpuApi {
804 device: &self.device,
805 queue: &self.queue,
806 renderer: &mut self.imgui.renderer,
807 },
808 _marker: PhantomData,
809 };
810
811 gui(&ui, &mut addons);
813
814 self.imgui
816 .platform
817 .prepare_render_with_ui(&ui, &self.window);
818
819 let draw_data = self.imgui.context.render();
820
821 let (frame, reconfigure_after_present) = match self.surface.get_current_texture() {
823 wgpu::CurrentSurfaceTexture::Success(frame) => (frame, false),
824 wgpu::CurrentSurfaceTexture::Suboptimal(frame) => (frame, true),
825 wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => {
826 self.surface.configure(&self.device, &self.surface_desc);
827 return Ok(());
828 }
829 wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
830 return Ok(());
831 }
832 wgpu::CurrentSurfaceTexture::Validation => {
833 return Err(DearAppError::SurfaceValidation);
834 }
835 };
836
837 let view = frame
838 .texture
839 .create_view(&wgpu::TextureViewDescriptor::default());
840 let mut encoder = self
841 .device
842 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
843 label: Some("Render Encoder"),
844 });
845
846 {
847 let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
848 label: Some("Render Pass"),
849 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
850 view: &view,
851 resolve_target: None,
852 ops: wgpu::Operations {
853 load: wgpu::LoadOp::Clear(self.clear_color),
854 store: wgpu::StoreOp::Store,
855 },
856 depth_slice: None,
857 })],
858 depth_stencil_attachment: None,
859 timestamp_writes: None,
860 occlusion_query_set: None,
861 multiview_mask: None,
862 });
863
864 self.imgui
865 .renderer
866 .new_frame()
867 .map_err(DearAppError::FramePrepare)?;
868 self.imgui
869 .renderer
870 .render_draw_data(draw_data, &mut rpass)
871 .map_err(DearAppError::Render)?;
872 }
873
874 self.queue.submit(Some(encoder.finish()));
875 frame.present();
876 if reconfigure_after_present {
877 self.surface.configure(&self.device, &self.surface_desc);
878 }
879 Ok(())
880 }
881}
882
883struct App<F>
884where
885 F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
886{
887 cfg: RunnerConfig,
888 addons_cfg: AddOnsConfig,
889 window: Option<AppWindow>,
890 gui: F,
891 cbs: RunnerCallbacks,
892 last_wake: Instant,
893}
894
895impl<F> App<F>
896where
897 F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
898{
899 fn new(cfg: RunnerConfig, addons_cfg: AddOnsConfig, cbs: RunnerCallbacks, gui: F) -> Self {
900 Self {
901 cfg,
902 addons_cfg,
903 window: None,
904 gui,
905 cbs,
906 last_wake: Instant::now(),
907 }
908 }
909}
910
911impl<F> ApplicationHandler for App<F>
912where
913 F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
914{
915 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
916 if self.window.is_none() {
917 match AppWindow::new(event_loop, &self.cfg, &self.addons_cfg, &mut self.cbs) {
918 Ok(window) => {
919 self.window = Some(window);
920 info!("Window created successfully");
921 if let Some(cb) = self.cbs.on_post_init.as_mut() {
922 if let Some(w) = self.window.as_mut() {
923 cb(&mut w.imgui.context);
924 }
925 }
926 if let Some(w) = self.window.as_ref() {
927 w.window.request_redraw();
928 }
929 }
930 Err(e) => {
931 error!("Failed to create window: {e}");
932 event_loop.exit();
933 }
934 }
935 }
936 }
937
938 fn window_event(
939 &mut self,
940 event_loop: &ActiveEventLoop,
941 window_id: WindowId,
942 event: WindowEvent,
943 ) {
944 match event {
947 WindowEvent::RedrawRequested => {
948 let mut need_recreate = false;
950 if let Some(window) = self.window.as_mut() {
951 let full_event: winit::event::Event<()> = winit::event::Event::WindowEvent {
952 window_id,
953 event: event.clone(),
954 };
955 if let Some(cb) = self.cbs.on_event.as_mut() {
956 cb(&full_event, &window.window, &mut window.imgui.context);
957 }
958 window.imgui.platform.handle_event(
959 &mut window.imgui.context,
960 &window.window,
961 &full_event,
962 );
963
964 if let Err(e) = window.render(&mut self.gui, &self.cfg.docking) {
965 error!("Render error: {e}; attempting to recover by recreating GPU state");
966 need_recreate = true;
967 } else if matches!(self.cfg.redraw, RedrawMode::Poll) {
968 window.window.request_redraw();
969 }
970 }
971
972 if need_recreate {
973 let mut old_window = self.window.take();
975 match AppWindow::new(event_loop, &self.cfg, &self.addons_cfg, &mut self.cbs) {
976 Ok(window) => {
977 self.window = Some(window);
978 info!("Successfully recreated window and GPU state after error");
979 if let Some(window) = self.window.as_mut() {
980 if let Some(cb) = self.cbs.on_post_init.as_mut() {
981 cb(&mut window.imgui.context);
982 }
983 window.window.request_redraw();
984 }
985 }
986 Err(e) => {
987 error!("Failed to recreate window after GPU error: {e}");
988 if let (Some(cb), Some(old)) =
989 (self.cbs.on_exit.as_mut(), old_window.as_mut())
990 {
991 cb(&mut old.imgui.context);
992 }
993 event_loop.exit();
994 }
995 }
996 }
997 }
998 _ => {
999 let window = match self.window.as_mut() {
1000 Some(window) => window,
1001 None => return,
1002 };
1003
1004 let full_event: winit::event::Event<()> = winit::event::Event::WindowEvent {
1005 window_id,
1006 event: event.clone(),
1007 };
1008 if let Some(cb) = self.cbs.on_event.as_mut() {
1009 cb(&full_event, &window.window, &mut window.imgui.context);
1010 }
1011 window.imgui.platform.handle_event(
1012 &mut window.imgui.context,
1013 &window.window,
1014 &full_event,
1015 );
1016
1017 match event {
1018 WindowEvent::Resized(physical_size) => {
1019 window.resize(physical_size);
1020 window.window.request_redraw();
1021 }
1022 WindowEvent::ScaleFactorChanged { .. } => {
1023 let new_size = window.window.inner_size();
1024 window.resize(new_size);
1025 window.window.request_redraw();
1026 }
1027 WindowEvent::CloseRequested => {
1028 if let Some(cb) = self.cbs.on_exit.as_mut() {
1029 if let Some(w) = self.window.as_mut() {
1030 cb(&mut w.imgui.context);
1031 }
1032 }
1033 event_loop.exit();
1034 }
1035 _ => {}
1036 }
1037 }
1038 }
1039 }
1040
1041 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
1042 match self.cfg.redraw {
1043 RedrawMode::Poll => {
1044 event_loop.set_control_flow(ControlFlow::Poll);
1045 if let Some(window) = &self.window {
1046 window.window.request_redraw();
1047 }
1048 }
1049 RedrawMode::Wait => {
1050 event_loop.set_control_flow(ControlFlow::Wait);
1051 }
1052 RedrawMode::WaitUntil { fps } => {
1053 let frame = Duration::from_secs_f32(1.0f32 / fps.max(1.0));
1054 let now = Instant::now();
1055 let mut next_wake = self.last_wake + frame;
1056 if now >= next_wake {
1057 self.last_wake = now;
1058 next_wake = self.last_wake + frame;
1059 if let Some(window) = &self.window {
1060 window.window.request_redraw();
1061 }
1062 }
1063 event_loop.set_control_flow(ControlFlow::WaitUntil(next_wake));
1064 }
1065 }
1066 }
1067}
1068
1069#[cfg(test)]
1070mod tests {
1071 use super::{AppBuilder, DearAppError};
1072
1073 #[test]
1074 fn app_builder_run_requires_frame_callback_without_starting_event_loop() {
1075 let err = AppBuilder::new()
1076 .run()
1077 .expect_err("builder without on_frame should fail before event loop startup");
1078
1079 assert!(matches!(err, DearAppError::MissingFrameCallback));
1080 assert_eq!(
1081 err.to_string(),
1082 "AppBuilder::run requires an on_frame callback"
1083 );
1084 }
1085}