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    _marker: PhantomData<&'a ()>,
101}
102
103/// Basic runner configuration
104pub struct RunnerConfig {
105    pub window_title: String,
106    pub window_size: (f64, f64),
107    pub present_mode: wgpu::PresentMode,
108    pub clear_color: [f32; 4],
109    pub docking: DockingConfig,
110    pub ini_filename: Option<PathBuf>,
111    pub restore_previous_geometry: bool,
112    pub redraw: RedrawMode,
113    /// Optional override for `Io::config_flags` in addition to docking flag.
114    /// If `Some`, it will be merged with docking flag; if `None`, only docking is applied.
115    pub io_config_flags: Option<ConfigFlags>,
116    /// Optional built-in theme to apply at startup (before on_style callback)
117    pub theme: Option<Theme>,
118}
119
120impl Default for RunnerConfig {
121    fn default() -> Self {
122        Self {
123            window_title: format!("Dear ImGui App - {}", env!("CARGO_PKG_VERSION")),
124            window_size: (1280.0, 720.0),
125            present_mode: wgpu::PresentMode::Fifo,
126            clear_color: [0.1, 0.2, 0.3, 1.0],
127            docking: DockingConfig::default(),
128            ini_filename: None,
129            restore_previous_geometry: true,
130            redraw: RedrawMode::Poll,
131            io_config_flags: None,
132            theme: None,
133        }
134    }
135}
136
137/// Docking configuration
138pub struct DockingConfig {
139    /// Enable ImGui docking (sets `ConfigFlags::DOCKING_ENABLE`)
140    pub enable: bool,
141    /// Automatically create a fullscreen host window + dockspace over main viewport
142    pub auto_dockspace: bool,
143    /// Flags used for the created dockspace
144    pub dockspace_flags: DockFlags,
145    /// Host window flags (for the fullscreen dockspace host)
146    pub host_window_flags: WindowFlags,
147    /// Optional host window name (useful to persist ini settings)
148    pub host_window_name: &'static str,
149}
150
151impl Default for DockingConfig {
152    fn default() -> Self {
153        Self {
154            enable: true,
155            auto_dockspace: true,
156            dockspace_flags: DockFlags::PASSTHRU_CENTRAL_NODE,
157            host_window_flags: WindowFlags::NO_TITLE_BAR
158                | WindowFlags::NO_RESIZE
159                | WindowFlags::NO_MOVE
160                | WindowFlags::NO_COLLAPSE
161                | WindowFlags::NO_BRING_TO_FRONT_ON_FOCUS
162                | WindowFlags::NO_NAV_FOCUS,
163            host_window_name: "DockSpaceHost",
164        }
165    }
166}
167
168/// Redraw behavior for the event loop
169#[derive(Clone, Copy, Debug)]
170pub enum RedrawMode {
171    /// Always redraw (ControlFlow::Poll)
172    Poll,
173    /// On-demand redraw (ControlFlow::Wait)
174    Wait,
175    /// Redraw at most `fps` per second using WaitUntil
176    WaitUntil { fps: f32 },
177}
178
179/// Simple built-in themes for convenience
180#[derive(Clone, Copy, Debug)]
181pub enum Theme {
182    Dark,
183    Light,
184    Classic,
185}
186
187fn apply_theme(_ctx: &mut imgui::Context, theme: Theme) {
188    // Apply via ImGui global helpers; doesn't require a Ui
189    unsafe {
190        match theme {
191            Theme::Dark => dear_imgui_rs::sys::igStyleColorsDark(std::ptr::null_mut()),
192            Theme::Light => dear_imgui_rs::sys::igStyleColorsLight(std::ptr::null_mut()),
193            Theme::Classic => dear_imgui_rs::sys::igStyleColorsClassic(std::ptr::null_mut()),
194        }
195    }
196}
197
198/// Runner lifecycle callbacks (all optional)
199pub struct RunnerCallbacks {
200    pub on_setup: Option<Box<dyn FnMut(&mut imgui::Context)>>,
201    pub on_style: Option<Box<dyn FnMut(&mut imgui::Context)>>,
202    pub on_fonts: Option<Box<dyn FnMut(&mut imgui::Context)>>,
203    pub on_post_init: Option<Box<dyn FnMut(&mut imgui::Context)>>,
204    pub on_event:
205        Option<Box<dyn FnMut(&winit::event::Event<()>, &Arc<Window>, &mut imgui::Context)>>,
206    pub on_exit: Option<Box<dyn FnMut(&mut imgui::Context)>>,
207}
208
209impl Default for RunnerCallbacks {
210    fn default() -> Self {
211        Self {
212            on_setup: None,
213            on_style: None,
214            on_fonts: None,
215            on_post_init: None,
216            on_event: None,
217            on_exit: None,
218        }
219    }
220}
221
222/// App builder for ergonomic configuration
223pub struct AppBuilder {
224    cfg: RunnerConfig,
225    addons: AddOnsConfig,
226    cbs: RunnerCallbacks,
227    on_frame: Option<Box<dyn FnMut(&imgui::Ui, &mut AddOns) + 'static>>,
228}
229
230impl AppBuilder {
231    pub fn new() -> Self {
232        Self {
233            cfg: RunnerConfig::default(),
234            addons: AddOnsConfig::default(),
235            cbs: RunnerCallbacks::default(),
236            on_frame: None,
237        }
238    }
239    pub fn with_config(mut self, cfg: RunnerConfig) -> Self {
240        self.cfg = cfg;
241        self
242    }
243    pub fn with_addons(mut self, addons: AddOnsConfig) -> Self {
244        self.addons = addons;
245        self
246    }
247    pub fn with_theme(mut self, theme: Theme) -> Self {
248        self.cfg.theme = Some(theme);
249        self
250    }
251    pub fn on_setup<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
252        self.cbs.on_setup = Some(Box::new(f));
253        self
254    }
255    pub fn on_style<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
256        self.cbs.on_style = Some(Box::new(f));
257        self
258    }
259    pub fn on_fonts<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
260        self.cbs.on_fonts = Some(Box::new(f));
261        self
262    }
263    pub fn on_post_init<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
264        self.cbs.on_post_init = Some(Box::new(f));
265        self
266    }
267    pub fn on_event<
268        F: FnMut(&winit::event::Event<()>, &Arc<Window>, &mut imgui::Context) + 'static,
269    >(
270        mut self,
271        f: F,
272    ) -> Self {
273        self.cbs.on_event = Some(Box::new(f));
274        self
275    }
276    pub fn on_frame<F: FnMut(&imgui::Ui, &mut AddOns) + 'static>(mut self, f: F) -> Self {
277        self.on_frame = Some(Box::new(f));
278        self
279    }
280    pub fn on_exit<F: FnMut(&mut imgui::Context) + 'static>(mut self, f: F) -> Self {
281        self.cbs.on_exit = Some(Box::new(f));
282        self
283    }
284    pub fn run(mut self) -> Result<(), DearAppError> {
285        let frame_fn = self
286            .on_frame
287            .take()
288            .ok_or_else(|| DearAppError::Generic("on_frame not set in AppBuilder".into()))?;
289        run_with_callbacks(self.cfg, self.addons, self.cbs, frame_fn)
290    }
291}
292
293/// Simple helper to run an app with a per-frame UI callback.
294///
295/// - Initializes Winit + WGPU + Dear ImGui
296/// - Optionally initializes add-ons (ImPlot, ImNodes)
297/// - Calls `gui` every frame with `Ui` and available add-ons
298pub fn run_simple<F>(mut gui: F) -> Result<(), DearAppError>
299where
300    F: FnMut(&imgui::Ui) + 'static,
301{
302    run(
303        RunnerConfig::default(),
304        AddOnsConfig::default(),
305        move |ui, _addons| gui(ui),
306    )
307}
308
309/// Run an app with configuration and add-ons.
310///
311/// The `gui` callback is called every frame with access to ImGui `Ui` and the initialized add-ons.
312pub fn run<F>(
313    runner: RunnerConfig,
314    addons_cfg: AddOnsConfig,
315    mut gui: F,
316) -> Result<(), DearAppError>
317where
318    F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
319{
320    run_with_callbacks(runner, addons_cfg, RunnerCallbacks::default(), gui)
321}
322
323/// Run with lifecycle callbacks (builder使用)
324pub fn run_with_callbacks<F>(
325    runner: RunnerConfig,
326    addons_cfg: AddOnsConfig,
327    cbs: RunnerCallbacks,
328    gui: F,
329) -> Result<(), DearAppError>
330where
331    F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
332{
333    let event_loop = EventLoop::new()?;
334    match runner.redraw {
335        RedrawMode::Poll => event_loop.set_control_flow(ControlFlow::Poll),
336        RedrawMode::Wait => event_loop.set_control_flow(ControlFlow::Wait),
337        RedrawMode::WaitUntil { .. } => event_loop.set_control_flow(ControlFlow::WaitUntil(
338            Instant::now() + Duration::from_millis(16),
339        )),
340    }
341
342    let mut app = App::new(runner, addons_cfg, cbs, gui);
343    info!("Starting Dear App event loop");
344    event_loop.run_app(&mut app)?;
345    Ok(())
346}
347
348struct ImguiState {
349    context: imgui::Context,
350    platform: imgui_winit::WinitPlatform,
351    renderer: imgui_wgpu::WgpuRenderer,
352    last_frame: Instant,
353}
354
355// Runtime docking flags controller
356pub struct DockingController {
357    flags: DockFlags,
358}
359
360pub struct DockingApi<'a> {
361    ctrl: &'a mut DockingController,
362}
363
364impl<'a> DockingApi<'a> {
365    pub fn flags(&self) -> DockFlags {
366        DockFlags::from_bits_retain(self.ctrl.flags.bits())
367    }
368    pub fn set_flags(&mut self, flags: DockFlags) {
369        self.ctrl.flags = flags;
370    }
371}
372
373struct AppWindow {
374    device: wgpu::Device,
375    queue: wgpu::Queue,
376    window: Arc<Window>,
377    surface_desc: wgpu::SurfaceConfiguration,
378    surface: wgpu::Surface<'static>,
379    imgui: ImguiState,
380
381    // add-ons
382    #[cfg(feature = "implot")]
383    implot_ctx: Option<implot::PlotContext>,
384    #[cfg(feature = "imnodes")]
385    imnodes_ctx: Option<imnodes::Context>,
386    #[cfg(feature = "implot3d")]
387    implot3d_ctx: Option<implot3d::Plot3DContext>,
388
389    // config for rendering
390    clear_color: wgpu::Color,
391    docking_ctrl: DockingController,
392}
393
394impl AppWindow {
395    fn new(
396        event_loop: &ActiveEventLoop,
397        cfg: &RunnerConfig,
398        addons: &AddOnsConfig,
399        cbs: &mut RunnerCallbacks,
400    ) -> Result<Self, DearAppError> {
401        // WGPU instance and window
402        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
403            backends: wgpu::Backends::PRIMARY,
404            ..Default::default()
405        });
406
407        let window = {
408            let size = LogicalSize::new(cfg.window_size.0, cfg.window_size.1);
409            Arc::new(
410                event_loop
411                    .create_window(
412                        Window::default_attributes()
413                            .with_title(cfg.window_title.clone())
414                            .with_inner_size(size),
415                    )
416                    .map_err(|e| DearAppError::Generic(format!("Window creation failed: {e}")))?,
417            )
418        };
419
420        let surface = instance
421            .create_surface(window.clone())
422            .map_err(|e| DearAppError::Generic(format!("Failed to create surface: {e}")))?;
423
424        let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
425            power_preference: wgpu::PowerPreference::HighPerformance,
426            compatible_surface: Some(&surface),
427            force_fallback_adapter: false,
428        }))
429        .expect("No suitable GPU adapter found");
430
431        let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor::default()))
432            .map_err(|e| DearAppError::Generic(format!("request_device failed: {e}")))?;
433
434        // Surface config
435        let physical_size = window.inner_size();
436        let caps = surface.get_capabilities(&adapter);
437        let preferred_srgb = [
438            wgpu::TextureFormat::Bgra8UnormSrgb,
439            wgpu::TextureFormat::Rgba8UnormSrgb,
440        ];
441        let format = preferred_srgb
442            .iter()
443            .cloned()
444            .find(|f| caps.formats.contains(f))
445            .unwrap_or(caps.formats[0]);
446
447        let surface_desc = wgpu::SurfaceConfiguration {
448            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
449            format,
450            width: physical_size.width,
451            height: physical_size.height,
452            present_mode: cfg.present_mode,
453            alpha_mode: wgpu::CompositeAlphaMode::Auto,
454            view_formats: vec![],
455            desired_maximum_frame_latency: 2,
456        };
457
458        surface.configure(&device, &surface_desc);
459
460        // ImGui setup
461        let mut context = imgui::Context::create();
462        // ini setup before fonts
463        if let Some(p) = &cfg.ini_filename {
464            let _ = context.set_ini_filename(Some(p.clone()));
465        } else {
466            let _ = context.set_ini_filename(None::<String>);
467        }
468
469        // lifecycle: on_setup/style/fonts before renderer init
470        if let Some(cb) = cbs.on_setup.as_mut() {
471            cb(&mut context);
472        }
473        // Apply optional theme from config before user style tweak
474        if let Some(theme) = cfg.theme {
475            apply_theme(&mut context, theme);
476        }
477        if let Some(cb) = cbs.on_style.as_mut() {
478            cb(&mut context);
479        }
480        if let Some(cb) = cbs.on_fonts.as_mut() {
481            cb(&mut context);
482        }
483
484        let mut platform = imgui_winit::WinitPlatform::new(&mut context);
485        platform.attach_window(&window, imgui_winit::HiDpiMode::Default, &mut context);
486
487        let init_info =
488            imgui_wgpu::WgpuInitInfo::new(device.clone(), queue.clone(), surface_desc.format);
489        let mut renderer = imgui_wgpu::WgpuRenderer::new(init_info, &mut context)
490            .map_err(|e| DearAppError::Generic(format!("Failed to init renderer: {e}")))?;
491        renderer.set_gamma_mode(imgui_wgpu::GammaMode::Auto);
492
493        // Configure IO flags & docking (never enable multi-viewport here)
494        {
495            let io = context.io_mut();
496            let mut flags = io.config_flags();
497            if cfg.docking.enable {
498                flags.insert(ConfigFlags::DOCKING_ENABLE);
499            }
500            if let Some(extra) = &cfg.io_config_flags {
501                let merged = flags.bits() | extra.bits();
502                flags = ConfigFlags::from_bits_retain(merged);
503            }
504            io.set_config_flags(flags);
505        }
506
507        #[cfg(feature = "implot")]
508        let implot_ctx = if addons.with_implot {
509            Some(implot::PlotContext::create(&context))
510        } else {
511            None
512        };
513
514        #[cfg(feature = "imnodes")]
515        let imnodes_ctx = if addons.with_imnodes {
516            Some(imnodes::Context::create(&context))
517        } else {
518            None
519        };
520
521        #[cfg(feature = "implot3d")]
522        let implot3d_ctx = if addons.with_implot3d {
523            Some(implot3d::Plot3DContext::create(&context))
524        } else {
525            None
526        };
527
528        let imgui = ImguiState {
529            context,
530            platform,
531            renderer,
532            last_frame: Instant::now(),
533        };
534
535        Ok(Self {
536            device,
537            queue,
538            window,
539            surface_desc,
540            surface,
541            imgui,
542            #[cfg(feature = "implot")]
543            implot_ctx,
544            #[cfg(feature = "imnodes")]
545            imnodes_ctx,
546            #[cfg(feature = "implot3d")]
547            implot3d_ctx,
548            clear_color: wgpu::Color {
549                r: cfg.clear_color[0] as f64,
550                g: cfg.clear_color[1] as f64,
551                b: cfg.clear_color[2] as f64,
552                a: cfg.clear_color[3] as f64,
553            },
554            docking_ctrl: DockingController {
555                flags: DockFlags::from_bits_retain(cfg.docking.dockspace_flags.bits()),
556            },
557        })
558    }
559
560    fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
561        if new_size.width > 0 && new_size.height > 0 {
562            self.surface_desc.width = new_size.width;
563            self.surface_desc.height = new_size.height;
564            self.surface.configure(&self.device, &self.surface_desc);
565        }
566    }
567
568    fn render<F>(&mut self, gui: &mut F, docking: &DockingConfig) -> Result<(), DearAppError>
569    where
570        F: FnMut(&imgui::Ui, &mut AddOns),
571    {
572        let now = Instant::now();
573        let delta_time = now - self.imgui.last_frame;
574        self.imgui
575            .context
576            .io_mut()
577            .set_delta_time(delta_time.as_secs_f32());
578        self.imgui.last_frame = now;
579
580        let frame = match self.surface.get_current_texture() {
581            Ok(frame) => frame,
582            Err(SurfaceError::Lost | SurfaceError::Outdated) => {
583                self.surface.configure(&self.device, &self.surface_desc);
584                return Ok(());
585            }
586            Err(SurfaceError::Timeout) => {
587                return Ok(());
588            }
589            Err(e) => return Err(DearAppError::from(e)),
590        };
591
592        self.imgui
593            .platform
594            .prepare_frame(&self.window, &mut self.imgui.context);
595        let ui = self.imgui.context.frame();
596
597        // Optional fullscreen dockspace
598        if docking.enable && docking.auto_dockspace {
599            let viewport = ui.main_viewport();
600            // Host window always covering the main viewport
601            ui.set_next_window_viewport(Id::from(viewport.id()));
602            let pos = viewport.pos();
603            let size = viewport.size();
604            // NO_BACKGROUND if passthru central node
605            let current_flags = DockFlags::from_bits_retain(self.docking_ctrl.flags.bits());
606            let mut win_flags = docking.host_window_flags;
607            if current_flags.contains(DockFlags::PASSTHRU_CENTRAL_NODE) {
608                win_flags |= WindowFlags::NO_BACKGROUND;
609            }
610            ui.window(docking.host_window_name)
611                .flags(win_flags)
612                .position([pos[0], pos[1]], imgui::Condition::Always)
613                .size([size[0], size[1]], imgui::Condition::Always)
614                .build(|| {
615                    let ds_flags = DockFlags::from_bits_retain(current_flags.bits());
616                    let _ = ui.dockspace_over_main_viewport_with_flags(Id::from(0u32), ds_flags);
617                });
618        }
619
620        // Build add-ons view
621        let mut addons = AddOns {
622            #[cfg(feature = "implot")]
623            implot: self.implot_ctx.as_ref(),
624            #[cfg(not(feature = "implot"))]
625            implot: None,
626            #[cfg(feature = "imnodes")]
627            imnodes: self.imnodes_ctx.as_ref(),
628            #[cfg(not(feature = "imnodes"))]
629            imnodes: None,
630            #[cfg(feature = "implot3d")]
631            implot3d: self.implot3d_ctx.as_ref(),
632            #[cfg(not(feature = "implot3d"))]
633            implot3d: None,
634            docking: DockingApi {
635                ctrl: &mut self.docking_ctrl,
636            },
637            _marker: PhantomData,
638        };
639
640        // Call user GUI
641        gui(&ui, &mut addons);
642
643        let view = frame
644            .texture
645            .create_view(&wgpu::TextureViewDescriptor::default());
646        let mut encoder = self
647            .device
648            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
649                label: Some("Render Encoder"),
650            });
651
652        let draw_data = self.imgui.context.render();
653
654        {
655            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
656                label: Some("Render Pass"),
657                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
658                    view: &view,
659                    resolve_target: None,
660                    ops: wgpu::Operations {
661                        load: wgpu::LoadOp::Clear(self.clear_color),
662                        store: wgpu::StoreOp::Store,
663                    },
664                    depth_slice: None,
665                })],
666                depth_stencil_attachment: None,
667                timestamp_writes: None,
668                occlusion_query_set: None,
669            });
670
671            self.imgui
672                .renderer
673                .new_frame()
674                .map_err(|e| DearAppError::Generic(format!("new_frame failed: {e}")))?;
675            self.imgui
676                .renderer
677                .render_draw_data(draw_data, &mut rpass)
678                .map_err(|e| DearAppError::Generic(format!("render_draw_data failed: {e}")))?;
679        }
680
681        self.queue.submit(Some(encoder.finish()));
682        frame.present();
683        Ok(())
684    }
685}
686
687struct App<F>
688where
689    F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
690{
691    cfg: RunnerConfig,
692    addons_cfg: AddOnsConfig,
693    window: Option<AppWindow>,
694    gui: F,
695    cbs: RunnerCallbacks,
696    last_wake: Instant,
697}
698
699impl<F> App<F>
700where
701    F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
702{
703    fn new(cfg: RunnerConfig, addons_cfg: AddOnsConfig, cbs: RunnerCallbacks, gui: F) -> Self {
704        Self {
705            cfg,
706            addons_cfg,
707            window: None,
708            gui,
709            cbs,
710            last_wake: Instant::now(),
711        }
712    }
713}
714
715impl<F> ApplicationHandler for App<F>
716where
717    F: FnMut(&imgui::Ui, &mut AddOns) + 'static,
718{
719    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
720        if self.window.is_none() {
721            match AppWindow::new(event_loop, &self.cfg, &self.addons_cfg, &mut self.cbs) {
722                Ok(window) => {
723                    self.window = Some(window);
724                    info!("Window created successfully");
725                    if let Some(cb) = self.cbs.on_post_init.as_mut() {
726                        if let Some(w) = self.window.as_mut() {
727                            cb(&mut w.imgui.context);
728                        }
729                    }
730                }
731                Err(e) => {
732                    error!("Failed to create window: {e}");
733                    event_loop.exit();
734                }
735            }
736        }
737    }
738
739    fn window_event(
740        &mut self,
741        event_loop: &ActiveEventLoop,
742        window_id: WindowId,
743        event: WindowEvent,
744    ) {
745        let window = match self.window.as_mut() {
746            Some(window) => window,
747            None => return,
748        };
749
750        let full_event: winit::event::Event<()> = winit::event::Event::WindowEvent {
751            window_id,
752            event: event.clone(),
753        };
754        if let Some(cb) = self.cbs.on_event.as_mut() {
755            cb(&full_event, &window.window, &mut window.imgui.context);
756        }
757        window
758            .imgui
759            .platform
760            .handle_event(&mut window.imgui.context, &window.window, &full_event);
761
762        match event {
763            WindowEvent::Resized(physical_size) => {
764                window.resize(physical_size);
765                window.window.request_redraw();
766            }
767            WindowEvent::ScaleFactorChanged { .. } => {
768                let new_size = window.window.inner_size();
769                window.resize(new_size);
770                window.window.request_redraw();
771            }
772            WindowEvent::CloseRequested => {
773                if let Some(cb) = self.cbs.on_exit.as_mut() {
774                    if let Some(w) = self.window.as_mut() {
775                        cb(&mut w.imgui.context);
776                    }
777                }
778                event_loop.exit();
779            }
780            WindowEvent::RedrawRequested => {
781                if let Err(e) = window.render(&mut self.gui, &self.cfg.docking) {
782                    error!("Render error: {e}");
783                }
784                window.window.request_redraw();
785            }
786            _ => {}
787        }
788    }
789
790    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
791        if let Some(window) = &self.window {
792            match self.cfg.redraw {
793                RedrawMode::Poll => {
794                    window.window.request_redraw();
795                }
796                RedrawMode::Wait => {
797                    // On-demand: redraw only on events; still keep UI alive
798                }
799                RedrawMode::WaitUntil { fps } => {
800                    let frame = (1.0f32 / fps.max(1.0)) as f32;
801                    if self.last_wake.elapsed() >= Duration::from_secs_f32(frame) {
802                        window.window.request_redraw();
803                        self.last_wake = Instant::now();
804                    }
805                }
806            }
807        }
808    }
809}