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