dear_app/
lib.rs

1//! dear-app: minimal Dear ImGui app runner for dear-imgui-rs
2//!
3//! Goals
4//! - Hide boilerplate (Winit + WGPU + platform + renderer)
5//! - Provide a simple per-frame closure API similar to `immapp::Run`
6//! - Optionally initialize add-ons (ImPlot, ImNodes) and expose them to the UI callback
7//!
8//! Quickstart
9//! ```no_run
10//! use dear_app::{run_simple};
11//! use dear_imgui_rs::*;
12//!
13//! fn main() {
14//!     run_simple(|ui| {
15//!         ui.window("Hello")
16//!             .size([300.0, 120.0], Condition::FirstUseEver)
17//!             .build(|| ui.text("Hello from dear-app!"));
18//!     }).unwrap();
19//! }
20//! ```
21use dear_imgui_rs as imgui;
22use dear_imgui_rs::{ConfigFlags, DockFlags, Id, 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 wgpu::SurfaceError;
33use winit::application::ApplicationHandler;
34use winit::dpi::LogicalSize;
35use winit::event::WindowEvent;
36use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
37use winit::window::{Window, WindowId};
38
39#[cfg(feature = "imnodes")]
40use dear_imnodes as imnodes;
41#[cfg(feature = "implot")]
42use dear_implot as implot;
43#[cfg(feature = "implot3d")]
44use dear_implot3d as implot3d;
45
46#[derive(Debug, Error)]
47pub enum DearAppError {
48    #[error("WGPU surface lost")]
49    SurfaceLost,
50    #[error("WGPU surface outdated")]
51    SurfaceOutdated,
52    #[error("WGPU surface timeout")]
53    SurfaceTimeout,
54    #[error("WGPU error: {0}")]
55    Wgpu(#[from] wgpu::SurfaceError),
56    #[error("Window creation error: {0}")]
57    WindowCreation(#[from] winit::error::EventLoopError),
58    #[error("Generic error: {0}")]
59    Generic(String),
60}
61
62/// Add-ons to be initialized and provided to the UI callback
63#[derive(Default, Clone, Copy)]
64pub struct AddOnsConfig {
65    pub with_implot: bool,
66    pub with_imnodes: bool,
67    pub with_implot3d: bool,
68}
69
70impl AddOnsConfig {
71    /// Enable add-ons that are compiled into this crate via features.
72    /// This does not fail if a given add-on is not enabled at compile time;
73    /// missing ones are simply ignored during initialization.
74    pub fn auto() -> Self {
75        Self {
76            with_implot: cfg!(feature = "implot"),
77            with_imnodes: cfg!(feature = "imnodes"),
78            with_implot3d: cfg!(feature = "implot3d"),
79        }
80    }
81}
82
83/// Mutable view to add-ons for per-frame rendering
84pub struct AddOns<'a> {
85    #[cfg(feature = "implot")]
86    pub implot: Option<&'a implot::PlotContext>,
87    #[cfg(not(feature = "implot"))]
88    pub implot: Option<()>,
89
90    #[cfg(feature = "imnodes")]
91    pub imnodes: Option<&'a imnodes::Context>,
92    #[cfg(not(feature = "imnodes"))]
93    pub imnodes: Option<()>,
94
95    #[cfg(feature = "implot3d")]
96    pub implot3d: Option<&'a implot3d::Plot3DContext>,
97    #[cfg(not(feature = "implot3d"))]
98    pub implot3d: Option<()>,
99    pub docking: DockingApi<'a>,
100    pub gpu: GpuApi<'a>,
101    _marker: PhantomData<&'a ()>,
102}
103
104/// Basic runner configuration
105pub struct RunnerConfig {
106    pub window_title: String,
107    pub window_size: (f64, f64),
108    pub present_mode: wgpu::PresentMode,
109    pub clear_color: [f32; 4],
110    pub docking: DockingConfig,
111    pub ini_filename: Option<PathBuf>,
112    pub restore_previous_geometry: bool,
113    pub redraw: RedrawMode,
114    /// Optional override for `Io::config_flags` in addition to docking flag.
115    /// If `Some`, it will be merged with docking flag; if `None`, only docking is applied.
116    pub io_config_flags: Option<ConfigFlags>,
117    /// Optional built-in theme to apply at startup (before on_style callback)
118    pub theme: Option<Theme>,
119}
120
121impl Default for RunnerConfig {
122    fn default() -> Self {
123        Self {
124            window_title: format!("Dear ImGui App - {}", env!("CARGO_PKG_VERSION")),
125            window_size: (1280.0, 720.0),
126            present_mode: wgpu::PresentMode::Fifo,
127            clear_color: [0.1, 0.2, 0.3, 1.0],
128            docking: DockingConfig::default(),
129            ini_filename: None,
130            restore_previous_geometry: true,
131            redraw: RedrawMode::Poll,
132            io_config_flags: None,
133            theme: None,
134        }
135    }
136}
137
138/// Docking configuration
139pub struct DockingConfig {
140    /// Enable ImGui docking (sets `ConfigFlags::DOCKING_ENABLE`)
141    pub enable: bool,
142    /// Automatically create a fullscreen host window + dockspace over main viewport
143    pub auto_dockspace: bool,
144    /// Flags used for the created dockspace
145    pub dockspace_flags: DockFlags,
146    /// Host window flags (for the fullscreen dockspace host)
147    pub host_window_flags: WindowFlags,
148    /// Optional host window name (useful to persist ini settings)
149    pub host_window_name: &'static str,
150}
151
152impl Default for DockingConfig {
153    fn default() -> Self {
154        Self {
155            enable: true,
156            auto_dockspace: true,
157            dockspace_flags: DockFlags::PASSTHRU_CENTRAL_NODE,
158            host_window_flags: WindowFlags::NO_TITLE_BAR
159                | WindowFlags::NO_RESIZE
160                | WindowFlags::NO_MOVE
161                | WindowFlags::NO_COLLAPSE
162                | WindowFlags::NO_BRING_TO_FRONT_ON_FOCUS
163                | WindowFlags::NO_NAV_FOCUS,
164            host_window_name: "DockSpaceHost",
165        }
166    }
167}
168
169/// Redraw behavior for the event loop
170#[derive(Clone, Copy, Debug)]
171pub enum RedrawMode {
172    /// Always redraw (ControlFlow::Poll)
173    Poll,
174    /// On-demand redraw (ControlFlow::Wait)
175    Wait,
176    /// Redraw at most `fps` per second using WaitUntil
177    WaitUntil { fps: f32 },
178}
179
180/// Simple built-in themes for convenience
181#[derive(Clone, Copy, Debug)]
182pub enum Theme {
183    Dark,
184    Light,
185    Classic,
186}
187
188fn apply_theme(_ctx: &mut imgui::Context, theme: Theme) {
189    // Apply via ImGui global helpers; doesn't require a Ui
190    unsafe {
191        match theme {
192            Theme::Dark => dear_imgui_rs::sys::igStyleColorsDark(std::ptr::null_mut()),
193            Theme::Light => dear_imgui_rs::sys::igStyleColorsLight(std::ptr::null_mut()),
194            Theme::Classic => dear_imgui_rs::sys::igStyleColorsClassic(std::ptr::null_mut()),
195        }
196    }
197}
198
199/// Runner lifecycle callbacks (all optional)
200pub struct RunnerCallbacks {
201    pub on_setup: Option<Box<dyn FnMut(&mut imgui::Context)>>,
202    pub on_style: Option<Box<dyn FnMut(&mut imgui::Context)>>,
203    pub on_fonts: Option<Box<dyn FnMut(&mut imgui::Context)>>,
204    pub on_post_init: Option<Box<dyn FnMut(&mut imgui::Context)>>,
205    pub on_event:
206        Option<Box<dyn FnMut(&winit::event::Event<()>, &Arc<Window>, &mut imgui::Context)>>,
207    pub on_exit: Option<Box<dyn FnMut(&mut imgui::Context)>>,
208}
209
210impl Default for RunnerCallbacks {
211    fn default() -> Self {
212        Self {
213            on_setup: None,
214            on_style: None,
215            on_fonts: None,
216            on_post_init: None,
217            on_event: None,
218            on_exit: None,
219        }
220    }
221}
222
223/// App builder for ergonomic configuration
224pub struct AppBuilder {
225    cfg: RunnerConfig,
226    addons: AddOnsConfig,
227    cbs: RunnerCallbacks,
228    on_frame: Option<Box<dyn FnMut(&imgui::Ui, &mut AddOns) + 'static>>,
229}
230
231impl AppBuilder {
232    pub fn new() -> Self {
233        Self {
234            cfg: RunnerConfig::default(),
235            addons: AddOnsConfig::default(),
236            cbs: RunnerCallbacks::default(),
237            on_frame: None,
238        }
239    }
240    pub fn with_config(mut self, cfg: RunnerConfig) -> Self {
241        self.cfg = cfg;
242        self
243    }
244    pub fn with_addons(mut self, addons: AddOnsConfig) -> Self {
245        self.addons = addons;
246        self
247    }
248    pub fn with_theme(mut self, theme: Theme) -> Self {
249        self.cfg.theme = Some(theme);
250        self
251    }
252    pub fn on_setup<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
253        self.cbs.on_setup = Some(Box::new(f));
254        self
255    }
256    pub fn on_style<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
257        self.cbs.on_style = Some(Box::new(f));
258        self
259    }
260    pub fn on_fonts<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
261        self.cbs.on_fonts = Some(Box::new(f));
262        self
263    }
264    pub fn on_post_init<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
265        self.cbs.on_post_init = Some(Box::new(f));
266        self
267    }
268    pub fn on_event<
269        F: FnMut(&winit::event::Event<()>, &Arc<Window>, &mut imgui::Context) + 'static,
270    >(
271        mut self,
272        f: F,
273    ) -> Self {
274        self.cbs.on_event = Some(Box::new(f));
275        self
276    }
277    pub fn on_frame<F: FnMut(&imgui::Ui, &mut AddOns) + 'static>(mut self, f: F) -> Self {
278        self.on_frame = Some(Box::new(f));
279        self
280    }
281    pub fn on_exit<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
282        self.cbs.on_exit = Some(Box::new(f));
283        self
284    }
285    pub fn run(mut self) -> Result<(), DearAppError> {
286        let frame_fn = self
287            .on_frame
288            .take()
289            .ok_or_else(|| DearAppError::Generic("on_frame not set in AppBuilder".into()))?;
290        run_with_callbacks(self.cfg, self.addons, self.cbs, frame_fn)
291    }
292}
293
294/// Simple helper to run an app with a per-frame UI callback.
295///
296/// - Initializes Winit + WGPU + Dear ImGui
297/// - Optionally initializes add-ons (ImPlot, ImNodes)
298/// - Calls `gui` every frame with `Ui` and available add-ons
299pub fn run_simple<F>(mut gui: F) -> Result<(), DearAppError>
300where
301    F: FnMut(&imgui::Ui) + 'static,
302{
303    run(
304        RunnerConfig::default(),
305        AddOnsConfig::default(),
306        move |ui, _addons| gui(ui),
307    )
308}
309
310/// Run an app with configuration and add-ons.
311///
312/// The `gui` callback is called every frame with access to ImGui `Ui` and the initialized add-ons.
313pub fn run<F>(
314    runner: RunnerConfig,
315    addons_cfg: AddOnsConfig,
316    mut gui: F,
317) -> Result<(), DearAppError>
318where
319    F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
320{
321    run_with_callbacks(runner, addons_cfg, RunnerCallbacks::default(), gui)
322}
323
324/// Run with explicit lifecycle callbacks (used by the builder-style API)
325pub fn run_with_callbacks<F>(
326    runner: RunnerConfig,
327    addons_cfg: AddOnsConfig,
328    cbs: RunnerCallbacks,
329    gui: F,
330) -> Result<(), DearAppError>
331where
332    F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
333{
334    let event_loop = EventLoop::new()?;
335    match runner.redraw {
336        RedrawMode::Poll => event_loop.set_control_flow(ControlFlow::Poll),
337        RedrawMode::Wait => event_loop.set_control_flow(ControlFlow::Wait),
338        RedrawMode::WaitUntil { .. } => event_loop.set_control_flow(ControlFlow::WaitUntil(
339            Instant::now() + Duration::from_millis(16),
340        )),
341    }
342
343    let mut app = App::new(runner, addons_cfg, cbs, gui);
344    info!("Starting Dear App event loop");
345    event_loop.run_app(&mut app)?;
346    Ok(())
347}
348
349struct ImguiState {
350    context: imgui::Context,
351    platform: imgui_winit::WinitPlatform,
352    renderer: imgui_wgpu::WgpuRenderer,
353    last_frame: Instant,
354}
355
356// Runtime docking flags controller
357pub struct DockingController {
358    flags: DockFlags,
359}
360
361pub struct DockingApi<'a> {
362    ctrl: &'a mut DockingController,
363}
364
365impl<'a> DockingApi<'a> {
366    pub fn flags(&self) -> DockFlags {
367        DockFlags::from_bits_retain(self.ctrl.flags.bits())
368    }
369    pub fn set_flags(&mut self, flags: DockFlags) {
370        self.ctrl.flags = flags;
371    }
372}
373
374// Minimal textures API to allow explicit texture updates from UI code
375/// GPU access API for real-time scenarios (game view, image browser, atlas editor)
376pub struct GpuApi<'a> {
377    device: &'a wgpu::Device,
378    queue: &'a wgpu::Queue,
379    renderer: &'a mut imgui_wgpu::WgpuRenderer,
380}
381
382impl<'a> GpuApi<'a> {
383    /// Access the WGPU device
384    pub fn device(&self) -> &wgpu::Device {
385        self.device
386    }
387    /// Access the default WGPU queue
388    pub fn queue(&self) -> &wgpu::Queue {
389        self.queue
390    }
391    /// Register an external texture + view and obtain an ImGui TextureId (u64)
392    pub fn register_texture(&mut self, texture: &wgpu::Texture, view: &wgpu::TextureView) -> u64 {
393        self.renderer.register_external_texture(texture, view)
394    }
395    /// Update the view for an existing registered texture
396    pub fn update_texture_view(&mut self, tex_id: u64, view: &wgpu::TextureView) -> bool {
397        self.renderer.update_external_texture_view(tex_id, view)
398    }
399    /// Unregister a previously registered texture
400    pub fn unregister_texture(&mut self, tex_id: u64) {
401        self.renderer.unregister_texture(tex_id)
402    }
403    /// Optional: directly drive managed TextureData create/update without waiting for draw pass
404    pub fn update_texture_data(
405        &mut self,
406        texture_data: &mut dear_imgui_rs::TextureData,
407    ) -> Result<(), String> {
408        let res = self
409            .renderer
410            .update_texture(texture_data)
411            .map_err(|e| format!("update_texture failed: {e}"))?;
412        // Important: apply result so TexID/Status are written back
413        res.apply_to(texture_data);
414        Ok(())
415    }
416}
417
418struct AppWindow {
419    device: wgpu::Device,
420    queue: wgpu::Queue,
421    window: Arc<Window>,
422    surface_desc: wgpu::SurfaceConfiguration,
423    surface: wgpu::Surface<'static>,
424    imgui: ImguiState,
425
426    // add-ons
427    #[cfg(feature = "implot")]
428    implot_ctx: Option<implot::PlotContext>,
429    #[cfg(feature = "imnodes")]
430    imnodes_ctx: Option<imnodes::Context>,
431    #[cfg(feature = "implot3d")]
432    implot3d_ctx: Option<implot3d::Plot3DContext>,
433
434    // config for rendering
435    clear_color: wgpu::Color,
436    docking_ctrl: DockingController,
437}
438
439impl AppWindow {
440    fn new(
441        event_loop: &ActiveEventLoop,
442        cfg: &RunnerConfig,
443        addons: &AddOnsConfig,
444        cbs: &mut RunnerCallbacks,
445    ) -> Result<Self, DearAppError> {
446        // WGPU instance and window
447        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
448            backends: wgpu::Backends::PRIMARY,
449            ..Default::default()
450        });
451
452        let window = {
453            let size = LogicalSize::new(cfg.window_size.0, cfg.window_size.1);
454            Arc::new(
455                event_loop
456                    .create_window(
457                        Window::default_attributes()
458                            .with_title(cfg.window_title.clone())
459                            .with_inner_size(size),
460                    )
461                    .map_err(|e| DearAppError::Generic(format!("Window creation failed: {e}")))?,
462            )
463        };
464
465        let surface = instance
466            .create_surface(window.clone())
467            .map_err(|e| DearAppError::Generic(format!("Failed to create surface: {e}")))?;
468
469        let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
470            power_preference: wgpu::PowerPreference::HighPerformance,
471            compatible_surface: Some(&surface),
472            force_fallback_adapter: false,
473        }))
474        .expect("No suitable GPU adapter found");
475
476        let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor::default()))
477            .map_err(|e| DearAppError::Generic(format!("request_device failed: {e}")))?;
478
479        // Surface config
480        let physical_size = window.inner_size();
481        let caps = surface.get_capabilities(&adapter);
482        let preferred_srgb = [
483            wgpu::TextureFormat::Bgra8UnormSrgb,
484            wgpu::TextureFormat::Rgba8UnormSrgb,
485        ];
486        let format = preferred_srgb
487            .iter()
488            .cloned()
489            .find(|f| caps.formats.contains(f))
490            .unwrap_or(caps.formats[0]);
491
492        let surface_desc = wgpu::SurfaceConfiguration {
493            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
494            format,
495            width: physical_size.width,
496            height: physical_size.height,
497            present_mode: cfg.present_mode,
498            alpha_mode: wgpu::CompositeAlphaMode::Auto,
499            view_formats: vec![],
500            desired_maximum_frame_latency: 2,
501        };
502
503        surface.configure(&device, &surface_desc);
504
505        // ImGui setup
506        let mut context = imgui::Context::create();
507        // ini setup before fonts
508        if let Some(p) = &cfg.ini_filename {
509            let _ = context.set_ini_filename(Some(p.clone()));
510        } else {
511            let _ = context.set_ini_filename(None::<String>);
512        }
513
514        // lifecycle: on_setup/style/fonts before renderer init
515        if let Some(cb) = cbs.on_setup.as_mut() {
516            cb(&mut context);
517        }
518        // Apply optional theme from config before user style tweak
519        if let Some(theme) = cfg.theme {
520            apply_theme(&mut context, theme);
521        }
522        if let Some(cb) = cbs.on_style.as_mut() {
523            cb(&mut context);
524        }
525        if let Some(cb) = cbs.on_fonts.as_mut() {
526            cb(&mut context);
527        }
528
529        let mut platform = imgui_winit::WinitPlatform::new(&mut context);
530        platform.attach_window(&window, imgui_winit::HiDpiMode::Default, &mut context);
531
532        let init_info =
533            imgui_wgpu::WgpuInitInfo::new(device.clone(), queue.clone(), surface_desc.format);
534        let mut renderer = imgui_wgpu::WgpuRenderer::new(init_info, &mut context)
535            .map_err(|e| DearAppError::Generic(format!("Failed to init renderer: {e}")))?;
536        renderer.set_gamma_mode(imgui_wgpu::GammaMode::Auto);
537
538        // Configure IO flags & docking (never enable multi-viewport here)
539        {
540            let io = context.io_mut();
541            let mut flags = io.config_flags();
542            if cfg.docking.enable {
543                flags.insert(ConfigFlags::DOCKING_ENABLE);
544            }
545            if let Some(extra) = &cfg.io_config_flags {
546                let merged = flags.bits() | extra.bits();
547                flags = ConfigFlags::from_bits_retain(merged);
548            }
549            io.set_config_flags(flags);
550        }
551
552        #[cfg(feature = "implot")]
553        let implot_ctx = if addons.with_implot {
554            Some(implot::PlotContext::create(&context))
555        } else {
556            None
557        };
558
559        #[cfg(feature = "imnodes")]
560        let imnodes_ctx = if addons.with_imnodes {
561            Some(imnodes::Context::create(&context))
562        } else {
563            None
564        };
565
566        #[cfg(feature = "implot3d")]
567        let implot3d_ctx = if addons.with_implot3d {
568            Some(implot3d::Plot3DContext::create(&context))
569        } else {
570            None
571        };
572
573        let imgui = ImguiState {
574            context,
575            platform,
576            renderer,
577            last_frame: Instant::now(),
578        };
579
580        Ok(Self {
581            device,
582            queue,
583            window,
584            surface_desc,
585            surface,
586            imgui,
587            #[cfg(feature = "implot")]
588            implot_ctx,
589            #[cfg(feature = "imnodes")]
590            imnodes_ctx,
591            #[cfg(feature = "implot3d")]
592            implot3d_ctx,
593            clear_color: wgpu::Color {
594                r: cfg.clear_color[0] as f64,
595                g: cfg.clear_color[1] as f64,
596                b: cfg.clear_color[2] as f64,
597                a: cfg.clear_color[3] as f64,
598            },
599            docking_ctrl: DockingController {
600                flags: DockFlags::from_bits_retain(cfg.docking.dockspace_flags.bits()),
601            },
602        })
603    }
604
605    fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
606        if new_size.width > 0 && new_size.height > 0 {
607            self.surface_desc.width = new_size.width;
608            self.surface_desc.height = new_size.height;
609            self.surface.configure(&self.device, &self.surface_desc);
610        }
611    }
612
613    fn render<F>(&mut self, gui: &mut F, docking: &DockingConfig) -> Result<(), DearAppError>
614    where
615        F: FnMut(&imgui::Ui, &mut AddOns),
616    {
617        let now = Instant::now();
618        let delta_time = now - self.imgui.last_frame;
619        self.imgui
620            .context
621            .io_mut()
622            .set_delta_time(delta_time.as_secs_f32());
623        self.imgui.last_frame = now;
624
625        let frame = match self.surface.get_current_texture() {
626            Ok(frame) => frame,
627            Err(SurfaceError::Lost | SurfaceError::Outdated) => {
628                self.surface.configure(&self.device, &self.surface_desc);
629                return Ok(());
630            }
631            Err(SurfaceError::Timeout) => {
632                return Ok(());
633            }
634            Err(e) => return Err(DearAppError::from(e)),
635        };
636
637        self.imgui
638            .platform
639            .prepare_frame(&self.window, &mut self.imgui.context);
640        let ui = self.imgui.context.frame();
641
642        // Optional fullscreen dockspace
643        if docking.enable && docking.auto_dockspace {
644            let viewport = ui.main_viewport();
645            // Host window always covering the main viewport
646            ui.set_next_window_viewport(Id::from(viewport.id()));
647            let pos = viewport.pos();
648            let size = viewport.size();
649            // NO_BACKGROUND if passthru central node
650            let current_flags = DockFlags::from_bits_retain(self.docking_ctrl.flags.bits());
651            let mut win_flags = docking.host_window_flags;
652            if current_flags.contains(DockFlags::PASSTHRU_CENTRAL_NODE) {
653                win_flags |= WindowFlags::NO_BACKGROUND;
654            }
655            ui.window(docking.host_window_name)
656                .flags(win_flags)
657                .position([pos[0], pos[1]], imgui::Condition::Always)
658                .size([size[0], size[1]], imgui::Condition::Always)
659                .build(|| {
660                    let ds_flags = DockFlags::from_bits_retain(current_flags.bits());
661                    let _ = ui.dockspace_over_main_viewport_with_flags(Id::from(0u32), ds_flags);
662                });
663        }
664
665        // Build add-ons view
666        let mut addons = AddOns {
667            #[cfg(feature = "implot")]
668            implot: self.implot_ctx.as_ref(),
669            #[cfg(not(feature = "implot"))]
670            implot: None,
671            #[cfg(feature = "imnodes")]
672            imnodes: self.imnodes_ctx.as_ref(),
673            #[cfg(not(feature = "imnodes"))]
674            imnodes: None,
675            #[cfg(feature = "implot3d")]
676            implot3d: self.implot3d_ctx.as_ref(),
677            #[cfg(not(feature = "implot3d"))]
678            implot3d: None,
679            docking: DockingApi {
680                ctrl: &mut self.docking_ctrl,
681            },
682            gpu: GpuApi {
683                device: &self.device,
684                queue: &self.queue,
685                renderer: &mut self.imgui.renderer,
686            },
687            _marker: PhantomData,
688        };
689
690        // Call user GUI
691        gui(&ui, &mut addons);
692
693        let view = frame
694            .texture
695            .create_view(&wgpu::TextureViewDescriptor::default());
696        let mut encoder = self
697            .device
698            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
699                label: Some("Render Encoder"),
700            });
701
702        let draw_data = self.imgui.context.render();
703
704        {
705            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
706                label: Some("Render Pass"),
707                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
708                    view: &view,
709                    resolve_target: None,
710                    ops: wgpu::Operations {
711                        load: wgpu::LoadOp::Clear(self.clear_color),
712                        store: wgpu::StoreOp::Store,
713                    },
714                    depth_slice: None,
715                })],
716                depth_stencil_attachment: None,
717                timestamp_writes: None,
718                occlusion_query_set: None,
719                multiview_mask: None,
720            });
721
722            self.imgui
723                .renderer
724                .new_frame()
725                .map_err(|e| DearAppError::Generic(format!("new_frame failed: {e}")))?;
726            self.imgui
727                .renderer
728                .render_draw_data(draw_data, &mut rpass)
729                .map_err(|e| DearAppError::Generic(format!("render_draw_data failed: {e}")))?;
730        }
731
732        self.queue.submit(Some(encoder.finish()));
733        frame.present();
734        Ok(())
735    }
736}
737
738struct App<F>
739where
740    F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
741{
742    cfg: RunnerConfig,
743    addons_cfg: AddOnsConfig,
744    window: Option<AppWindow>,
745    gui: F,
746    cbs: RunnerCallbacks,
747    last_wake: Instant,
748}
749
750impl<F> App<F>
751where
752    F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
753{
754    fn new(cfg: RunnerConfig, addons_cfg: AddOnsConfig, cbs: RunnerCallbacks, gui: F) -> Self {
755        Self {
756            cfg,
757            addons_cfg,
758            window: None,
759            gui,
760            cbs,
761            last_wake: Instant::now(),
762        }
763    }
764}
765
766impl<F> ApplicationHandler for App<F>
767where
768    F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
769{
770    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
771        if self.window.is_none() {
772            match AppWindow::new(event_loop, &self.cfg, &self.addons_cfg, &mut self.cbs) {
773                Ok(window) => {
774                    self.window = Some(window);
775                    info!("Window created successfully");
776                    if let Some(cb) = self.cbs.on_post_init.as_mut() {
777                        if let Some(w) = self.window.as_mut() {
778                            cb(&mut w.imgui.context);
779                        }
780                    }
781                }
782                Err(e) => {
783                    error!("Failed to create window: {e}");
784                    event_loop.exit();
785                }
786            }
787        }
788    }
789
790    fn window_event(
791        &mut self,
792        event_loop: &ActiveEventLoop,
793        window_id: WindowId,
794        event: WindowEvent,
795    ) {
796        // We may recreate the window/gpu stack on fatal GPU errors, so we avoid
797        // holding a mutable borrow of self.window across the whole match.
798        match event {
799            WindowEvent::RedrawRequested => {
800                // Render and, on fatal errors, attempt a full GPU/window rebuild.
801                let mut need_recreate = false;
802                if let Some(window) = self.window.as_mut() {
803                    let full_event: winit::event::Event<()> = winit::event::Event::WindowEvent {
804                        window_id,
805                        event: event.clone(),
806                    };
807                    if let Some(cb) = self.cbs.on_event.as_mut() {
808                        cb(&full_event, &window.window, &mut window.imgui.context);
809                    }
810                    window.imgui.platform.handle_event(
811                        &mut window.imgui.context,
812                        &window.window,
813                        &full_event,
814                    );
815
816                    if let Err(e) = window.render(&mut self.gui, &self.cfg.docking) {
817                        error!("Render error: {e}; attempting to recover by recreating GPU state");
818                        need_recreate = true;
819                    } else {
820                        window.window.request_redraw();
821                    }
822                }
823
824                if need_recreate {
825                    // Drop the existing window and try to rebuild the whole stack.
826                    self.window = None;
827                    if let Err(e) =
828                        AppWindow::new(event_loop, &self.cfg, &self.addons_cfg, &mut self.cbs)
829                    {
830                        error!("Failed to recreate window after GPU error: {e}");
831                        if let Some(cb) = self.cbs.on_exit.as_mut() {
832                            // Best-effort: give user a chance to clean up the old context if any.
833                            if let Some(w) = self.window.as_mut() {
834                                cb(&mut w.imgui.context);
835                            }
836                        }
837                        event_loop.exit();
838                    } else if let Some(window) = self.window.as_mut() {
839                        info!("Successfully recreated window and GPU state after error");
840                        if let Some(cb) = self.cbs.on_post_init.as_mut() {
841                            cb(&mut window.imgui.context);
842                        }
843                        window.window.request_redraw();
844                    }
845                }
846            }
847            _ => {
848                let window = match self.window.as_mut() {
849                    Some(window) => window,
850                    None => return,
851                };
852
853                let full_event: winit::event::Event<()> = winit::event::Event::WindowEvent {
854                    window_id,
855                    event: event.clone(),
856                };
857                if let Some(cb) = self.cbs.on_event.as_mut() {
858                    cb(&full_event, &window.window, &mut window.imgui.context);
859                }
860                window.imgui.platform.handle_event(
861                    &mut window.imgui.context,
862                    &window.window,
863                    &full_event,
864                );
865
866                match event {
867                    WindowEvent::Resized(physical_size) => {
868                        window.resize(physical_size);
869                        window.window.request_redraw();
870                    }
871                    WindowEvent::ScaleFactorChanged { .. } => {
872                        let new_size = window.window.inner_size();
873                        window.resize(new_size);
874                        window.window.request_redraw();
875                    }
876                    WindowEvent::CloseRequested => {
877                        if let Some(cb) = self.cbs.on_exit.as_mut() {
878                            if let Some(w) = self.window.as_mut() {
879                                cb(&mut w.imgui.context);
880                            }
881                        }
882                        event_loop.exit();
883                    }
884                    _ => {}
885                }
886            }
887        }
888    }
889
890    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
891        if let Some(window) = &self.window {
892            match self.cfg.redraw {
893                RedrawMode::Poll => {
894                    window.window.request_redraw();
895                }
896                RedrawMode::Wait => {
897                    // On-demand: redraw only on events; still keep UI alive
898                }
899                RedrawMode::WaitUntil { fps } => {
900                    let frame = (1.0f32 / fps.max(1.0)) as f32;
901                    if self.last_wake.elapsed() >= Duration::from_secs_f32(frame) {
902                        window.window.request_redraw();
903                        self.last_wake = Instant::now();
904                    }
905                }
906            }
907        }
908    }
909}