dear-app 0.16.0

Generation-aware Dear ImGui application runtime for dear-imgui-rs on Winit and WGPU
Documentation

dear-app

Crates.io Documentation

dear-app is the Winit + WGPU runtime for dear-imgui-rs. It keeps the main window, Dear ImGui context, add-ons, and user application alive while replacing only GPU-owned state after device loss.

Quick Start

Create a binary crate and add the stable release:

[dependencies]
dear-app = "0.16"

Then use dear_app::run_ui for applications that only need persistent UI state:

use dear_app::{AppConfig, RunError, imgui::Condition, run_ui};

fn main() -> Result<(), RunError> {
    let mut clicks = 0;

    run_ui(AppConfig::default(), move |ui| {
        ui.window("Hello")
            .size([360.0, 160.0], Condition::FirstUseEver)
            .build(|| {
                if ui.button("Click me") {
                    clicks += 1;
                }
                ui.same_line();
                ui.text(format!("Clicks: {clicks}"));
            });
    })
}

Run it with cargo run. dear-app re-exports the matching core crate as dear_app::imgui, so this starter cannot accidentally combine incompatible versions.

Fallible Frame Closure

Move the same captured state to run_frame when a small application needs fallible work, explicit exit, compiled add-ons, or the active GPU generation but does not need lifecycle hooks:

use std::io;

use dear_app::{AppConfig, RunError, run_frame};

fn main() -> Result<(), RunError> {
    let mut clicks = 0_u64;

    run_frame(AppConfig::default(), move |context| {
        let mut exit_requested = false;
        let mut failure_requested = false;
        let ui = context.ui();
        ui.window("Editor").build(|| {
            if ui.button("Increment") {
                clicks = clicks.saturating_add(1);
            }
            ui.text(format!("Clicks: {clicks}"));
            exit_requested = ui.button("Exit");
            failure_requested = ui.button("Return an error");
        });

        if exit_requested {
            context.request_exit();
        }
        if failure_requested {
            return Err(io::Error::other("the frame requested a user error"));
        }
        Ok(())
    })
}

The closure value remains alive across frames and GPU recovery. FrameContext::addons() exposes the add-ons compiled and enabled by AppConfig::addons, while FrameContext::gpu() exposes the current generation-aware WGPU API. request_exit() is normal control flow: the runtime performs shutdown exactly once and returns Ok(()). A returned error wins even if the same callback also requested exit; RunError::Application records ApplicationStage::Frame and retains the original error through std::error::Error::source.

The complete runnable example is:

cargo run -p dear-imgui-examples --bin fallible_frame

Application Lifecycle

Move from run_frame to Application only when the program needs context initialization, raw window events, GPU resources, device-loss recovery, or deterministic teardown. The frame body can move unchanged into Application::frame:

use dear_app::{AppConfig, Application, FrameContext, RunError};

struct Editor {
    frames: u64,
}

impl Application for Editor {
    fn frame(&mut self, context: &mut FrameContext<'_>) -> Result<(), RunError> {
        self.frames += 1;
        let ui = context.ui();
        ui.window("Editor").build(|| {
            ui.text(format!("Frame {}", self.frames));
        });
        Ok(())
    }
}

fn main() -> Result<(), RunError> {
    dear_app::run(AppConfig::default(), Editor { frames: 0 })
}

Application is the single owner of application state:

  • configure_imgui runs once before renderer initialization.
  • initialized runs once after the first GPU generation is ready.
  • event receives events only for the live main window.
  • prepare_frame mutates Context-owned resources before the next frame opens.
  • frame is the only per-frame UI callback.
  • gpu_lost runs before old GPU resources are invalidated.
  • gpu_recreated runs after a replacement generation is committed.
  • shutdown runs once before add-ons and the Dear ImGui context are destroyed.

InitContext exposes the complete Dear ImGui Context only from configure_imgui, before platform and renderer attachment, for extension APIs that require it. initialized receives InitializedContext; runtime event, pre-frame, and shutdown hooks likewise expose only narrow IO, style, font-atlas, and managed-texture capabilities. The runtime compares the Context identity, native frame sequence, and lifecycle state at every callback boundary, so opening and closing a frame inside a callback is still reported as RunError::ImGuiFrameOwnership.

Every failing hook is wrapped with its typed ApplicationStage, while the hook's original RunError remains in the source chain. The first actual failure remains primary. Shutdown still runs exactly once, and a shutdown or backend-release failure is returned only when no earlier runtime or event-loop failure exists.

The feature-free lifecycle example implements every hook and displays real hook activity, persistent state, and the active GPU generation:

cargo run -p dear-imgui-examples --bin application_lifecycle

Surface loss and resize do not recreate the application or Dear ImGui context. dear-app acquires and, when necessary, recovers the main surface before calling any per-frame application hook. Timeout and occlusion skip the frame without advancing application or test state. Only WGPU device loss starts GPU recovery, and that callback communicates with the UI thread through the Winit event loop.

Test Engine

Enable the optional test-engine feature when the application owns an interactive Dear ImGui Test Engine. The matching crate is available as dear_app::test_engine; return its TestEngine from Application::test_engine. The runtime then owns the complete render -> pre-swap -> present -> post-swap transaction for every admitted frame. Applications must not call presentation hooks themselves.

Standalone test programs that own their whole frame loop should use TestRunner::run_graphical instead. Headless runs use the explicitly virtual presentation path and do not claim graphical swap coverage.

Configuration

Configuration is a value, not a builder. Docking is opt-in:

use dear_app::{AddOnsConfig, AppConfig, DockingConfig, Theme, WgpuConfig, WgpuPreset};

let config = AppConfig {
    window_title: "Editor".to_owned(),
    theme: Some(Theme::Dark),
    docking: DockingConfig::full_viewport(),
    addons: AddOnsConfig::auto(),
    wgpu: WgpuConfig::from_preset(WgpuPreset::HighPerformance),
    ..Default::default()
};

DockingConfig configures docking in the main window; it does not enable Dear ImGui platform multi-viewport. dear-app rejects ConfigFlags::VIEWPORTS_ENABLE in 0.16 because its single-window recovery model does not own secondary platform windows. Use the Winit or SDL3 owning runtime examples when an application needs native secondary windows.

GPU Resources

External WGPU textures return an ExternalTextureHandle instead of a raw TextureId. Resolve the handle through the current frame's GpuApi before submitting it to Dear ImGui. Resolution fails after GPU recovery, so an old identifier cannot alias a resource from a newer generation.

Rebuild application-owned GPU resources from gpu_recreated. Managed Dear ImGui textures retain their CPU data and are reset to WantCreate before the old renderer is torn down.

Add-ons

The implot, imnodes, and implot3d features create extension contexts owned by the stable UI state. Access them through FrameContext::addons() from run_frame or Application::frame.