Skip to main content

dear_app/
lib.rs

1//! Winit + WGPU application runtime for `dear-imgui-rs`.
2//!
3//! The runtime keeps the main window, Dear ImGui context, and user [`Application`] stable while
4//! replacing only GPU-owned state after a device-loss signal.
5//!
6//! ```no_run
7//! use dear_app::AppConfig;
8//!
9//! dear_app::run_ui(AppConfig::default(), |ui| {
10//!     ui.window("Hello").build(|| ui.text("Hello, world!"));
11//! })?;
12//! # Ok::<(), dear_app::RunError>(())
13//! ```
14
15use std::{convert::Infallible, error::Error, marker::PhantomData};
16
17mod application;
18mod config;
19mod runtime;
20
21pub use application::{
22    AddOns, Application, ApplicationStage, DockingApi, EventContext, ExternalTextureError,
23    ExternalTextureHandle, FrameContext, GpuApi, GpuContext, GpuGeneration, InitContext,
24    InitializedContext, PrepareFrameContext, RunError, ShutdownContext,
25};
26pub use config::{
27    AddOnsConfig, AppConfig, DockingConfig, RedrawMode, Theme, WgpuConfig, WgpuPreset,
28};
29pub use dear_imgui_rs as imgui;
30#[cfg(feature = "test-engine")]
31pub use dear_imgui_test_engine as test_engine;
32pub use wgpu;
33
34/// Runs one persistent application until the event loop exits.
35pub fn run<A: Application + 'static>(config: AppConfig, application: A) -> Result<(), RunError> {
36    runtime::run(config, application)
37}
38
39/// Runs an application with one fallible, exit-capable frame closure.
40///
41/// The closure value is retained by the runtime for the complete event-loop lifetime, so captured
42/// state persists between frames and across GPU generation recovery. [`FrameContext`] exposes the
43/// current UI, compiled add-ons, GPU generation, and [`FrameContext::request_exit`]. Returning an
44/// error preserves its [`Error::source`] chain and attributes it to [`ApplicationStage::Frame`].
45/// Requesting exit is a normal control signal and returns `Ok(())` after exactly-once shutdown.
46///
47/// Use [`run_ui`] when the callback only needs [`imgui::Ui`]. Use [`run`] when initialization,
48/// event, GPU recovery, or shutdown hooks are required.
49pub fn run_frame<F, E>(config: AppConfig, frame: F) -> Result<(), RunError>
50where
51    F: for<'frame> FnMut(&mut FrameContext<'frame>) -> Result<(), E> + 'static,
52    E: Error + 'static,
53{
54    run(config, FrameApplication::<F, E>::new(frame))
55}
56
57struct FrameApplication<F, E> {
58    frame: F,
59    _error: PhantomData<fn() -> E>,
60}
61
62impl<F, E> FrameApplication<F, E>
63where
64    E: Error + 'static,
65{
66    fn new(frame: F) -> Self {
67        Self {
68            frame,
69            _error: PhantomData,
70        }
71    }
72
73    fn invoke<C>(&mut self, context: &mut C) -> Result<(), RunError>
74    where
75        F: FnMut(&mut C) -> Result<(), E>,
76    {
77        (self.frame)(context)
78            .map_err(|source| RunError::application(ApplicationStage::Frame, source))
79    }
80}
81
82impl<F, E> Application for FrameApplication<F, E>
83where
84    F: for<'frame> FnMut(&mut FrameContext<'frame>) -> Result<(), E> + 'static,
85    E: Error + 'static,
86{
87    fn frame(&mut self, context: &mut FrameContext<'_>) -> Result<(), RunError> {
88        self.invoke(context)
89    }
90}
91
92/// Runs an application whose persistent state is captured by one UI closure.
93///
94/// This is the smallest entry point for applications that only build UI. Move to [`run_frame`]
95/// when the closure needs fallibility, exit control, add-ons, or the current GPU generation. Use
96/// [`run`] with an [`Application`] implementation when initialization, events, GPU recovery, or
97/// teardown hooks are required. All three entries use the same runtime and recovery state machine.
98pub fn run_ui<F>(config: AppConfig, mut ui: F) -> Result<(), RunError>
99where
100    F: FnMut(&imgui::Ui) + 'static,
101{
102    run_frame(config, move |context| {
103        (ui)(context.ui());
104        Ok::<(), Infallible>(())
105    })
106}
107
108#[cfg(test)]
109mod tests {
110    use std::{cell::Cell, error::Error as _, rc::Rc};
111
112    use thiserror::Error;
113
114    use super::{ApplicationStage, FrameApplication, RunError};
115
116    #[derive(Debug, Error)]
117    #[error("injected frame failure")]
118    struct FrameFailure;
119
120    #[derive(Default)]
121    struct ProbeFrameContext {
122        exit_requested: bool,
123    }
124
125    #[test]
126    fn frame_application_retains_closure_state_between_calls() {
127        let calls = Rc::new(Cell::new(0));
128        let observed_calls = Rc::clone(&calls);
129        let mut application = FrameApplication::<_, FrameFailure>::new(
130            move |value: &mut usize| -> Result<(), FrameFailure> {
131                let call = observed_calls.get() + 1;
132                observed_calls.set(call);
133                *value += call;
134                Ok(())
135            },
136        );
137        let mut value = 0;
138
139        application.invoke(&mut value).unwrap();
140        application.invoke(&mut value).unwrap();
141
142        assert_eq!(calls.get(), 2);
143        assert_eq!(value, 3);
144    }
145
146    #[test]
147    fn frame_application_preserves_user_error_as_the_stage_source() {
148        let mut application =
149            FrameApplication::<_, FrameFailure>::new(|_context: &mut ()| Err(FrameFailure));
150
151        let mut context = ();
152        let error = application.invoke(&mut context).unwrap_err();
153
154        assert!(matches!(
155            &error,
156            RunError::Application {
157                stage: ApplicationStage::Frame,
158                ..
159            }
160        ));
161        assert!(
162            error
163                .source()
164                .and_then(|source| source.downcast_ref::<FrameFailure>())
165                .is_some()
166        );
167    }
168
169    #[test]
170    fn exit_request_is_successful_control_flow() {
171        let mut application =
172            FrameApplication::<_, FrameFailure>::new(|context: &mut ProbeFrameContext| {
173                context.exit_requested = true;
174                Ok(())
175            });
176        let mut context = ProbeFrameContext::default();
177
178        assert!(application.invoke(&mut context).is_ok());
179        assert!(context.exit_requested);
180    }
181
182    #[test]
183    fn frame_error_remains_primary_after_an_exit_request() {
184        let mut application =
185            FrameApplication::<_, FrameFailure>::new(|context: &mut ProbeFrameContext| {
186                context.exit_requested = true;
187                Err(FrameFailure)
188            });
189        let mut context = ProbeFrameContext::default();
190
191        let error = application.invoke(&mut context).unwrap_err();
192
193        assert!(context.exit_requested);
194        assert_eq!(error.application_stage(), Some(ApplicationStage::Frame));
195    }
196}