Skip to main content

dear_app/
application.rs

1use std::{error::Error as StdError, fmt};
2
3use dear_imgui_rs as imgui;
4use dear_imgui_rs::{DockNodeFlags, TextureId};
5use thiserror::Error;
6use winit::{event::WindowEvent, window::Window};
7
8use crate::AppConfig;
9
10#[cfg(feature = "imnodes")]
11use dear_imnodes as imnodes;
12#[cfg(feature = "implot")]
13use dear_implot as implot;
14#[cfg(feature = "implot3d")]
15use dear_implot3d as implot3d;
16
17/// Monotonically increasing identity of the active GPU resource set.
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub struct GpuGeneration(pub(crate) u64);
20
21impl GpuGeneration {
22    pub const INITIAL: Self = Self(0);
23
24    #[must_use]
25    pub const fn get(self) -> u64 {
26        self.0
27    }
28
29    pub(crate) const fn checked_next(self) -> Option<Self> {
30        match self.0.checked_add(1) {
31            Some(value) => Some(Self(value)),
32            None => None,
33        }
34    }
35
36    fn ensure_current(self, current: Self) -> Result<(), ExternalTextureError> {
37        if self == current {
38            Ok(())
39        } else {
40            Err(ExternalTextureError::StaleGeneration {
41                handle_generation: self.get(),
42                current_generation: current.get(),
43            })
44        }
45    }
46}
47
48/// Opaque external texture identity bound to one GPU generation.
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50pub struct ExternalTextureHandle {
51    id: dear_imgui_wgpu::ExternalTextureId,
52    generation: GpuGeneration,
53}
54
55impl ExternalTextureHandle {
56    #[must_use]
57    pub const fn generation(self) -> GpuGeneration {
58        self.generation
59    }
60
61    fn resolve_for_generation(
62        self,
63        current: GpuGeneration,
64    ) -> Result<dear_imgui_wgpu::ExternalTextureId, ExternalTextureError> {
65        self.generation.ensure_current(current)?;
66        Ok(self.id)
67    }
68}
69
70#[derive(Debug, Error)]
71#[non_exhaustive]
72pub enum ExternalTextureError {
73    #[error(
74        "external texture belongs to GPU generation {handle_generation}, current generation is {current_generation}"
75    )]
76    StaleGeneration {
77        handle_generation: u64,
78        current_generation: u64,
79    },
80    #[error(transparent)]
81    Renderer(#[from] dear_imgui_wgpu::RendererError),
82}
83
84/// Lifecycle hook that produced an [`Application`] error.
85///
86/// The runtime assigns this stage at the hook boundary. Applications therefore do not need to
87/// encode callback names in strings, and callers can inspect failures without parsing messages.
88#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
89#[non_exhaustive]
90pub enum ApplicationStage {
91    ConfigureImgui,
92    Initialized,
93    Event,
94    PrepareFrame,
95    Frame,
96    GpuLost,
97    GpuRecreated,
98    Shutdown,
99}
100
101impl ApplicationStage {
102    #[must_use]
103    pub const fn as_str(self) -> &'static str {
104        match self {
105            Self::ConfigureImgui => "configure_imgui",
106            Self::Initialized => "initialized",
107            Self::Event => "event",
108            Self::PrepareFrame => "prepare_frame",
109            Self::Frame => "frame",
110            Self::GpuLost => "gpu_lost",
111            Self::GpuRecreated => "gpu_recreated",
112            Self::Shutdown => "shutdown",
113        }
114    }
115}
116
117impl fmt::Display for ApplicationStage {
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        formatter.write_str(self.as_str())
120    }
121}
122
123#[cfg(test)]
124#[derive(Debug, Error)]
125#[error("{0}")]
126struct ApplicationMessage(String);
127
128#[derive(Debug, Error)]
129#[non_exhaustive]
130pub enum RunError {
131    #[error("event loop error: {0}")]
132    EventLoop(#[from] winit::error::EventLoopError),
133    #[error("window creation failed: {0}")]
134    WindowCreation(#[source] winit::error::OsError),
135    #[error("WGPU surface creation failed: {0}")]
136    SurfaceCreation(#[source] wgpu::CreateSurfaceError),
137    #[error("no suitable WGPU adapter found: {0}")]
138    AdapterUnavailable(#[source] wgpu::RequestAdapterError),
139    #[error("the selected surface exposes no texture formats")]
140    SurfaceFormatUnavailable,
141    #[error(
142        "the recreated surface changed format from {previous:?} to {replacement:?}; the active renderer cannot be reused"
143    )]
144    SurfaceFormatChanged {
145        previous: wgpu::TextureFormat,
146        replacement: wgpu::TextureFormat,
147    },
148    #[error("WGPU device request failed: {0}")]
149    DeviceRequest(#[source] wgpu::RequestDeviceError),
150    #[error("Dear ImGui context creation failed: {0}")]
151    ImGuiContext(#[source] imgui::ImGuiError),
152    #[error("Dear ImGui frame admission failed: {0}")]
153    ImGuiFrame(#[source] imgui::render::RendererConsumerError),
154    #[error(
155        "application callback during {stage} changed Dear ImGui frame ownership from {before:?} to {after:?}"
156    )]
157    ImGuiFrameOwnership {
158        stage: ApplicationStage,
159        before: imgui::FrameLifecycleStamp,
160        after: imgui::FrameLifecycleStamp,
161    },
162    #[error("WGPU renderer initialization failed: {0}")]
163    RendererInit(#[source] dear_imgui_wgpu::RendererError),
164    #[error("WGPU renderer draw failed: {0}")]
165    Render(#[source] dear_imgui_wgpu::RendererError),
166    #[error("WGPU resource invalidation failed: {0}")]
167    GpuInvalidation(#[source] dear_imgui_wgpu::RendererError),
168    #[error("WGPU renderer release failed: {0}")]
169    RendererRelease(#[source] dear_imgui_wgpu::RendererError),
170    #[error("Dear ImGui dockspace submission failed: {0}")]
171    Dockspace(#[source] imgui::DockspaceError),
172    #[error("Winit platform operation `{operation}` failed: {source}")]
173    Platform {
174        operation: &'static str,
175        #[source]
176        source: dear_imgui_winit::WinitPlatformError,
177    },
178    #[error(
179        "dear-app does not support Dear ImGui platform viewports; remove ConfigFlags::VIEWPORTS_ENABLE"
180    )]
181    MultiViewportUnsupported,
182    #[error("WGPU surface validation failed while acquiring the next frame")]
183    SurfaceValidation,
184    #[error("WGPU exhausted GPU memory: {message}")]
185    GpuOutOfMemory { message: String },
186    #[error("WGPU reported an uncaptured validation error: {message}")]
187    GpuValidation { message: String },
188    #[error("WGPU reported an uncaptured internal error: {message}")]
189    GpuInternal { message: String },
190    #[cfg(feature = "test-engine")]
191    #[error("Test Engine frame {frame} failed: {source}")]
192    TestEngineFrame {
193        frame: u64,
194        #[source]
195        source: Box<dear_imgui_test_engine::FrameDriverError<RunError, RunError, RunError>>,
196    },
197    #[error("application callback failed during {stage}: {source}")]
198    Application {
199        stage: ApplicationStage,
200        #[source]
201        source: Box<dyn StdError + 'static>,
202    },
203    #[error("GPU generation recovery failed: {message}")]
204    Recovery { message: String },
205}
206
207impl RunError {
208    /// Wraps an application-owned error with the lifecycle stage that produced it.
209    #[must_use]
210    pub fn application<E>(stage: ApplicationStage, source: E) -> Self
211    where
212        E: StdError + 'static,
213    {
214        Self::Application {
215            stage,
216            source: Box::new(source),
217        }
218    }
219
220    /// Returns the lifecycle stage for an application callback failure.
221    #[must_use]
222    pub const fn application_stage(&self) -> Option<ApplicationStage> {
223        match self {
224            Self::Application { stage, .. } | Self::ImGuiFrameOwnership { stage, .. } => {
225                Some(*stage)
226            }
227            _ => None,
228        }
229    }
230
231    #[cfg(test)]
232    pub(crate) fn application_message(stage: ApplicationStage, message: impl Into<String>) -> Self {
233        Self::application(stage, ApplicationMessage(message.into()))
234    }
235
236    pub(crate) fn during_application_stage(self, stage: ApplicationStage) -> Self {
237        if self.application_stage() == Some(stage) {
238            self
239        } else {
240            Self::application(stage, self)
241        }
242    }
243}
244
245/// Persistent user application. This value survives every GPU recreation.
246///
247/// The runtime wraps every hook failure with the matching [`ApplicationStage`] while retaining
248/// the returned [`RunError`] as its source. Hooks therefore return the most specific error they
249/// have instead of formatting callback names into messages.
250pub trait Application {
251    /// Configures the one stable Dear ImGui context before renderer initialization.
252    fn configure_imgui(&mut self, _context: &mut InitContext<'_>) -> Result<(), RunError> {
253        Ok(())
254    }
255
256    /// Runs exactly once after the window, UI state, and first GPU generation are ready.
257    fn initialized(
258        &mut self,
259        _context: &mut InitializedContext<'_>,
260        _gpu: &mut GpuContext<'_>,
261    ) -> Result<(), RunError> {
262        Ok(())
263    }
264
265    /// Runs before the old GPU generation is invalidated and destroyed.
266    fn gpu_lost(&mut self, _context: &mut GpuContext<'_>) -> Result<(), RunError> {
267        Ok(())
268    }
269
270    /// Runs after a replacement GPU generation has been committed.
271    fn gpu_recreated(&mut self, _context: &mut GpuContext<'_>) -> Result<(), RunError> {
272        Ok(())
273    }
274
275    /// Receives events for the live main window only.
276    fn event(&mut self, _context: &mut EventContext<'_>) -> Result<(), RunError> {
277        Ok(())
278    }
279
280    /// Mutates Context-owned resources before Dear ImGui opens the next frame.
281    fn prepare_frame(&mut self, _context: &mut PrepareFrameContext<'_>) -> Result<(), RunError> {
282        Ok(())
283    }
284
285    /// Builds one Dear ImGui frame.
286    fn frame(&mut self, context: &mut FrameContext<'_>) -> Result<(), RunError>;
287
288    /// Returns the Test Engine attached to this application's Context, when enabled.
289    ///
290    /// The runtime drives this engine only for admitted surface frames and owns the complete
291    /// `render -> pre-swap -> present -> post-swap` protocol. Applications may use the engine from
292    /// [`Self::frame`] for UI and queue controls, but must not drive presentation independently.
293    #[cfg(feature = "test-engine")]
294    fn test_engine(&mut self) -> Option<&mut crate::test_engine::TestEngine> {
295        None
296    }
297
298    /// Runs exactly once before add-ons and the Dear ImGui context are torn down.
299    fn shutdown(&mut self, _context: &mut ShutdownContext<'_>) -> Result<(), RunError> {
300        Ok(())
301    }
302}
303
304pub struct InitContext<'a> {
305    pub(crate) imgui: &'a mut imgui::Context,
306    pub(crate) window: &'a Window,
307    pub(crate) config: &'a AppConfig,
308}
309
310impl InitContext<'_> {
311    /// Returns the Context before platform and renderer ownership is attached.
312    ///
313    /// This full capability exists for extension crates whose initialization API requires
314    /// `&mut Context`. The runtime validates the Context identity and frame progress when the
315    /// callback returns. Later lifecycle hooks expose only narrow capabilities.
316    pub fn imgui(&mut self) -> &mut imgui::Context {
317        self.imgui
318    }
319
320    #[must_use]
321    pub fn window(&self) -> &Window {
322        self.window
323    }
324
325    #[must_use]
326    pub fn config(&self) -> &AppConfig {
327        self.config
328    }
329}
330
331/// Post-attachment initialization capabilities without Context or frame ownership.
332///
333/// ```compile_fail
334/// use dear_app::InitializedContext;
335///
336/// fn replace_context(context: &mut InitializedContext<'_>) {
337///     let _ = std::mem::replace(context.imgui(), dear_app::imgui::Context::create());
338/// }
339/// ```
340pub struct InitializedContext<'a> {
341    pub(crate) imgui: &'a mut imgui::Context,
342    pub(crate) window: &'a Window,
343    pub(crate) config: &'a AppConfig,
344}
345
346impl InitializedContext<'_> {
347    #[must_use]
348    pub fn window(&self) -> &Window {
349        self.window
350    }
351
352    #[must_use]
353    pub fn config(&self) -> &AppConfig {
354        self.config
355    }
356}
357
358/// Event-time capabilities that cannot open or render a Dear ImGui frame.
359///
360/// Frame lifecycle APIs are intentionally unavailable at this boundary:
361///
362/// ```compile_fail
363/// use dear_app::EventContext;
364///
365/// fn steal_frame(context: &mut EventContext<'_>) {
366///     context.imgui().begin_frame();
367/// }
368/// ```
369pub struct EventContext<'a> {
370    pub(crate) event: &'a WindowEvent,
371    pub(crate) imgui: &'a mut imgui::Context,
372    pub(crate) window: &'a Window,
373    pub(crate) exit_requested: &'a mut bool,
374}
375
376impl EventContext<'_> {
377    #[must_use]
378    pub fn event(&self) -> &WindowEvent {
379        self.event
380    }
381
382    #[must_use]
383    pub fn window(&self) -> &Window {
384        self.window
385    }
386
387    /// Requests normal event-loop exit after the current event callback completes.
388    pub fn request_exit(&mut self) {
389        *self.exit_requested = true;
390    }
391}
392
393/// Terminal resource capabilities without Context or frame ownership.
394///
395/// ```compile_fail
396/// use dear_app::ShutdownContext;
397///
398/// fn replace_context(context: &mut ShutdownContext<'_>) {
399///     let _ = std::mem::replace(context.imgui(), dear_app::imgui::Context::create());
400/// }
401/// ```
402pub struct ShutdownContext<'a> {
403    pub(crate) imgui: &'a mut imgui::Context,
404    pub(crate) window: &'a Window,
405    pub(crate) generation: Option<GpuGeneration>,
406}
407
408/// Pre-frame access to Context-owned resources without frame lifecycle authority.
409///
410/// Resource mutations are allowed here, but opening or rendering the frame remains owned by the
411/// runtime:
412///
413/// ```compile_fail
414/// use dear_app::PrepareFrameContext;
415///
416/// fn steal_frame(context: &mut PrepareFrameContext<'_>) {
417///     context.imgui().begin_frame();
418/// }
419/// ```
420pub struct PrepareFrameContext<'a> {
421    pub(crate) imgui: &'a mut imgui::Context,
422    pub(crate) window: &'a Window,
423}
424
425impl PrepareFrameContext<'_> {
426    #[must_use]
427    pub fn window(&self) -> &Window {
428        self.window
429    }
430}
431
432macro_rules! impl_context_resource_access {
433    ($context:ident) => {
434        impl $context<'_> {
435            /// Borrows Dear ImGui IO without exposing frame lifecycle operations.
436            #[must_use]
437            pub fn io(&self) -> &imgui::Io {
438                self.imgui.io()
439            }
440
441            /// Mutates Dear ImGui IO without exposing frame lifecycle operations.
442            pub fn io_mut(&mut self) -> &mut imgui::Io {
443                self.imgui.io_mut()
444            }
445
446            /// Borrows the global Dear ImGui style.
447            #[must_use]
448            pub fn style(&self) -> &imgui::Style {
449                self.imgui.style()
450            }
451
452            /// Mutates the global Dear ImGui style.
453            pub fn style_mut(&mut self) -> &mut imgui::Style {
454                self.imgui.style_mut()
455            }
456
457            /// Borrows the Context-owned font atlas.
458            #[must_use]
459            pub fn font_atlas(&self) -> &imgui::FontAtlas {
460                self.imgui.font_atlas()
461            }
462
463            /// Transfers an owned texture into the Context-managed texture registry.
464            pub fn register_texture(
465                &mut self,
466                texture: imgui::OwnedTextureData,
467            ) -> imgui::ManagedTextureId {
468                self.imgui.register_texture(texture)
469            }
470
471            /// Reads a managed texture through a non-escaping capability.
472            pub fn with_texture<R>(
473                &self,
474                id: imgui::ManagedTextureId,
475                operation: impl for<'texture> FnOnce(imgui::ManagedTextureRef<'texture>) -> R,
476            ) -> Result<R, imgui::ManagedTextureError> {
477                self.imgui.with_texture(id, operation)
478            }
479
480            /// Mutates a managed texture through a non-escaping capability.
481            pub fn with_texture_mut<R>(
482                &mut self,
483                id: imgui::ManagedTextureId,
484                operation: impl for<'texture> FnOnce(imgui::ManagedTextureMut<'texture>) -> R,
485            ) -> Result<R, imgui::ManagedTextureError> {
486                self.imgui.with_texture_mut(id, operation)
487            }
488
489            /// Mutates a managed texture while flattening access and data-validation errors.
490            pub fn try_with_texture_mut<R>(
491                &mut self,
492                id: imgui::ManagedTextureId,
493                operation: impl for<'texture> FnOnce(
494                    imgui::ManagedTextureMut<'texture>,
495                ) -> Result<R, imgui::TextureDataError>,
496            ) -> Result<R, imgui::ManagedTextureMutationError> {
497                self.imgui.try_with_texture_mut(id, operation)
498            }
499
500            /// Retires a managed texture after outstanding renderer work completes.
501            pub fn remove_texture(
502                &mut self,
503                id: imgui::ManagedTextureId,
504            ) -> Result<(), imgui::ManagedTextureError> {
505                self.imgui.remove_texture(id)
506            }
507        }
508    };
509}
510
511impl_context_resource_access!(EventContext);
512impl_context_resource_access!(InitializedContext);
513impl_context_resource_access!(PrepareFrameContext);
514impl_context_resource_access!(ShutdownContext);
515
516impl ShutdownContext<'_> {
517    #[must_use]
518    pub fn window(&self) -> &Window {
519        self.window
520    }
521
522    #[must_use]
523    pub const fn gpu_generation(&self) -> Option<GpuGeneration> {
524        self.generation
525    }
526}
527
528pub struct DockingController {
529    pub(crate) flags: DockNodeFlags,
530}
531
532pub struct DockingApi<'a> {
533    pub(crate) controller: &'a mut DockingController,
534}
535
536impl DockingApi<'_> {
537    #[must_use]
538    pub fn flags(&self) -> DockNodeFlags {
539        DockNodeFlags::from_bits_retain(self.controller.flags.bits())
540    }
541
542    pub fn set_flags(&mut self, flags: DockNodeFlags) {
543        self.controller.flags = flags;
544    }
545}
546
547pub struct AddOns<'a> {
548    #[cfg(feature = "implot")]
549    pub(crate) implot: Option<&'a implot::PlotContext>,
550    #[cfg(feature = "imnodes")]
551    pub(crate) imnodes: Option<&'a imnodes::Context>,
552    #[cfg(feature = "implot3d")]
553    pub(crate) implot3d: Option<&'a implot3d::Plot3DContext>,
554    pub(crate) docking: DockingApi<'a>,
555}
556
557impl<'a> AddOns<'a> {
558    #[cfg(feature = "implot")]
559    #[must_use]
560    pub fn implot(&self) -> Option<&'a implot::PlotContext> {
561        self.implot
562    }
563
564    #[cfg(feature = "imnodes")]
565    #[must_use]
566    pub fn imnodes(&self) -> Option<&'a imnodes::Context> {
567        self.imnodes
568    }
569
570    #[cfg(feature = "implot3d")]
571    #[must_use]
572    pub fn implot3d(&self) -> Option<&'a implot3d::Plot3DContext> {
573        self.implot3d
574    }
575
576    pub fn docking(&mut self) -> &mut DockingApi<'a> {
577        &mut self.docking
578    }
579}
580
581pub struct GpuApi<'a> {
582    pub(crate) device: &'a wgpu::Device,
583    pub(crate) queue: &'a wgpu::Queue,
584    pub(crate) renderer: &'a mut dear_imgui_wgpu::WgpuRenderer,
585    pub(crate) generation: GpuGeneration,
586}
587
588impl GpuApi<'_> {
589    #[must_use]
590    pub fn device(&self) -> &wgpu::Device {
591        self.device
592    }
593
594    #[must_use]
595    pub fn queue(&self) -> &wgpu::Queue {
596        self.queue
597    }
598
599    #[must_use]
600    pub const fn generation(&self) -> GpuGeneration {
601        self.generation
602    }
603
604    pub fn register_external_texture(
605        &mut self,
606        view: &wgpu::TextureView,
607    ) -> Result<ExternalTextureHandle, ExternalTextureError> {
608        Ok(ExternalTextureHandle {
609            id: self.renderer.register_external_texture(view)?,
610            generation: self.generation,
611        })
612    }
613
614    pub fn resolve_external_texture(
615        &self,
616        handle: ExternalTextureHandle,
617    ) -> Result<TextureId, ExternalTextureError> {
618        handle
619            .resolve_for_generation(self.generation)
620            .map(dear_imgui_wgpu::ExternalTextureId::texture_id)
621    }
622
623    pub fn update_external_texture(
624        &mut self,
625        handle: ExternalTextureHandle,
626        view: &wgpu::TextureView,
627    ) -> Result<(), ExternalTextureError> {
628        let texture = handle.resolve_for_generation(self.generation)?;
629        self.renderer.update_external_texture(texture, view)?;
630        Ok(())
631    }
632
633    pub fn unregister_external_texture(
634        &mut self,
635        handle: ExternalTextureHandle,
636    ) -> Result<(), ExternalTextureError> {
637        let texture = handle.resolve_for_generation(self.generation)?;
638        self.renderer.unregister_external_texture(texture)?;
639        Ok(())
640    }
641}
642
643pub struct GpuContext<'a> {
644    pub(crate) window: &'a Window,
645    pub(crate) surface_config: &'a wgpu::SurfaceConfiguration,
646    pub(crate) gpu: GpuApi<'a>,
647}
648
649impl<'a> GpuContext<'a> {
650    #[must_use]
651    pub fn window(&self) -> &Window {
652        self.window
653    }
654
655    #[must_use]
656    pub fn surface_config(&self) -> &wgpu::SurfaceConfiguration {
657        self.surface_config
658    }
659
660    pub fn gpu(&mut self) -> &mut GpuApi<'a> {
661        &mut self.gpu
662    }
663
664    #[must_use]
665    pub const fn generation(&self) -> GpuGeneration {
666        self.gpu.generation
667    }
668}
669
670pub struct FrameContext<'a> {
671    pub(crate) ui: &'a imgui::Ui,
672    pub(crate) addons: AddOns<'a>,
673    pub(crate) gpu: GpuApi<'a>,
674    pub(crate) exit_requested: &'a mut bool,
675}
676
677impl<'a> FrameContext<'a> {
678    #[must_use]
679    pub fn ui(&self) -> &'a imgui::Ui {
680        self.ui
681    }
682
683    pub fn addons(&mut self) -> &mut AddOns<'a> {
684        &mut self.addons
685    }
686
687    pub fn gpu(&mut self) -> &mut GpuApi<'a> {
688        &mut self.gpu
689    }
690
691    /// Requests normal event-loop exit after the current frame is presented.
692    ///
693    /// This is a control signal, not an error. If the same callback returns an error, that error
694    /// remains primary and shutdown still runs exactly once.
695    pub fn request_exit(&mut self) {
696        *self.exit_requested = true;
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use std::error::Error as _;
703
704    use thiserror::Error;
705
706    use super::{ApplicationStage, ExternalTextureError, GpuGeneration, RunError};
707
708    #[derive(Debug, Error)]
709    #[error("injected user failure")]
710    struct UserError;
711
712    #[test]
713    fn stale_generation_error_names_both_epochs() {
714        let error = ExternalTextureError::StaleGeneration {
715            handle_generation: 2,
716            current_generation: 3,
717        };
718
719        assert_eq!(
720            error.to_string(),
721            "external texture belongs to GPU generation 2, current generation is 3"
722        );
723    }
724
725    #[test]
726    fn stale_gpu_generation_is_rejected_after_recovery() {
727        assert!(matches!(
728            GpuGeneration(2).ensure_current(GpuGeneration(3)),
729            Err(ExternalTextureError::StaleGeneration {
730                handle_generation: 2,
731                current_generation: 3,
732            })
733        ));
734        assert!(GpuGeneration(3).ensure_current(GpuGeneration(3)).is_ok());
735    }
736
737    #[test]
738    fn application_error_retains_typed_stage_and_original_source() {
739        let error = RunError::application(ApplicationStage::Frame, UserError);
740
741        assert!(matches!(
742            &error,
743            RunError::Application {
744                stage: ApplicationStage::Frame,
745                ..
746            }
747        ));
748        assert!(
749            error
750                .source()
751                .and_then(|source| source.downcast_ref::<UserError>())
752                .is_some()
753        );
754    }
755
756    #[test]
757    fn assigning_the_same_stage_does_not_duplicate_the_source_chain() {
758        let error = RunError::application(ApplicationStage::Frame, UserError)
759            .during_application_stage(ApplicationStage::Frame);
760
761        assert!(
762            error
763                .source()
764                .and_then(|source| source.downcast_ref::<UserError>())
765                .is_some()
766        );
767    }
768
769    #[test]
770    fn frame_ownership_error_reports_its_application_stage() {
771        let _guard = crate::runtime::imgui_test_guard();
772        let context = dear_imgui_rs::Context::create();
773        let stamp = context.frame_lifecycle_stamp();
774        let error = RunError::ImGuiFrameOwnership {
775            stage: ApplicationStage::PrepareFrame,
776            before: stamp,
777            after: stamp,
778        };
779
780        assert_eq!(
781            error.application_stage(),
782            Some(ApplicationStage::PrepareFrame)
783        );
784    }
785
786    #[test]
787    fn runtime_assigned_stage_retains_the_hook_run_error_as_source() {
788        let error = RunError::GpuValidation {
789            message: "injected validation failure".to_owned(),
790        }
791        .during_application_stage(ApplicationStage::GpuRecreated);
792
793        assert!(matches!(
794            &error,
795            RunError::Application {
796                stage: ApplicationStage::GpuRecreated,
797                ..
798            }
799        ));
800        assert!(matches!(
801            error
802                .source()
803                .and_then(|source| source.downcast_ref::<RunError>()),
804            Some(RunError::GpuValidation { message })
805                if message == "injected validation failure"
806        ));
807    }
808}