Skip to main content

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