Skip to main content

dear_imgui_rs/context/
suspended.rs

1use std::panic::{self, AssertUnwindSafe};
2use std::ptr;
3
4use crate::clipboard::ClipboardContext;
5use crate::fonts::SharedFontAtlas;
6use crate::sys;
7
8use super::Context;
9use super::attachment::AttachmentRegistry;
10use super::binding::{
11    CTX_MUTEX, ContextBinding, ContextId, ContextState, ContextThreadLease,
12    bound_context_scope_active, clear_current_context, no_current_context, set_current_context,
13};
14use super::frame::FrameLifecycleState;
15use super::snapshot_hub::SnapshotHub;
16use super::texture_registry::ManagedTextureRegistry;
17use super::{
18    ContextActivationError, ContextActivationReason, ContextScopeError, ContextSuspensionError,
19    ContextSuspensionReason, ScopedActivationError,
20};
21
22impl Context {
23    /// Suspends this Context so another Context can become active.
24    ///
25    /// Rejection retains this Context in [`ContextSuspensionError`], allowing the caller to end an
26    /// open frame, leave a binding scope, or otherwise repair the conflict and retry.
27    pub fn suspend(self) -> Result<SuspendedContext, ContextSuspensionError> {
28        let _guard = CTX_MUTEX.lock();
29        if bound_context_scope_active() {
30            return Err(ContextSuspensionError::new(
31                self,
32                ContextSuspensionReason::BindingScopeActive,
33            ));
34        }
35        if !self.is_current_context() {
36            return Err(ContextSuspensionError::new(
37                self,
38                ContextSuspensionReason::NotCurrent,
39            ));
40        }
41        if self.frame_lifecycle_state_unlocked() == FrameLifecycleState::InFrame {
42            return Err(ContextSuspensionError::new(
43                self,
44                ContextSuspensionReason::FrameOpen,
45            ));
46        }
47        clear_current_context();
48        Ok(SuspendedContext(self))
49    }
50
51    /// Suspends this Context or panics with the rejection reason.
52    ///
53    /// # Panics
54    ///
55    /// Panics if a Context binding scope is active, this Context is not current, or a frame is
56    /// still open. Use [`Context::suspend`] when any of those states is recoverable.
57    pub fn suspend_or_panic(self) -> SuspendedContext {
58        self.suspend()
59            .unwrap_or_else(|error| panic!("Context::suspend_or_panic(): {error}"))
60    }
61}
62
63/// A suspended Dear ImGui context
64///
65/// A suspended context retains its state, but is not usable without activating it first.
66#[derive(Debug)]
67pub struct SuspendedContext(pub(super) Context);
68
69impl SuspendedContext {
70    /// Returns the process-unique identity of this Context.
71    pub fn id(&self) -> ContextId {
72        self.0.id()
73    }
74
75    /// Runs a closure while this suspended Context is active.
76    ///
77    /// No other Context or Context binding scope may be active. This makes the closure's
78    /// `&mut Context` the only safe live Context owner in the process, so it cannot be exchanged
79    /// with another owner while native `GImGui` points at it. An open frame left behind when the
80    /// closure returns `Err` or panics is ended before propagating that outcome.
81    ///
82    /// Admission conflicts and a successful closure that leaves a frame open are returned as
83    /// [`ScopedActivationError::Scope`] containing a [`ContextScopeError`]. A closure error is
84    /// wrapped in [`ScopedActivationError::Closure`]. The borrowed suspended owner remains
85    /// available for every returned error.
86    ///
87    /// # Panics
88    ///
89    /// Resumes a panic raised by the closure with its original payload. It also panics if the
90    /// closure replaces the complete Context owner while native `GImGui` still points at the
91    /// original Context; ordinary safe code should not attempt that owner exchange.
92    pub fn try_with_active<T, E>(
93        &mut self,
94        f: impl FnOnce(&mut Context) -> Result<T, E>,
95    ) -> Result<T, ScopedActivationError<E>> {
96        let _guard = CTX_MUTEX.lock();
97        if bound_context_scope_active() {
98            return Err(
99                ContextScopeError::Activation(ContextActivationReason::BindingScopeActive).into(),
100            );
101        }
102        if !no_current_context() {
103            return Err(ContextScopeError::Activation(
104                ContextActivationReason::ContextAlreadyActive,
105            )
106            .into());
107        }
108        let expected_id = self.0.id();
109        let expected_raw = self.0.raw;
110        let binding = self.0.binding();
111        binding
112            .try_with_bound_context_guarded(|bound| {
113                let result = panic::catch_unwind(AssertUnwindSafe(|| f(&mut self.0)));
114
115                debug_assert!(bound.previous_context().is_null());
116                if self.0.id() != expected_id || self.0.raw != expected_raw {
117                    if let Err(payload) = result {
118                        panic::resume_unwind(payload);
119                    }
120                    panic!(
121                        "SuspendedContext::try_with_active(): closure moved or replaced the Context owner"
122                    );
123                }
124
125                match result {
126                    Ok(Ok(value)) => {
127                        if self.0.end_frame_for_teardown_unlocked() {
128                            return Err(ContextScopeError::FrameLeftOpen.into());
129                        }
130                        Ok(value)
131                    }
132                    Ok(Err(error)) => {
133                        self.0.end_frame_for_teardown_unlocked();
134                        Err(ScopedActivationError::Closure(error))
135                    }
136                    Err(payload) => {
137                        // Cleanup must not replace the closure's panic payload.
138                        let _ = panic::catch_unwind(AssertUnwindSafe(|| {
139                            self.0.end_frame_for_teardown_unlocked();
140                        }));
141                        panic::resume_unwind(payload)
142                    }
143                }
144            })
145            .map_err(|error| {
146                ScopedActivationError::Scope(ContextScopeError::ContextUnavailable(error))
147            })?
148    }
149
150    /// Runs a closure while this suspended Context is active, panicking on scope errors.
151    ///
152    /// # Panics
153    ///
154    /// Panics if another Context or binding scope is active, if the closure leaves a frame open,
155    /// if the Context cannot be bound, or if the closure itself panics.
156    pub fn with_active_or_panic<T>(&mut self, f: impl FnOnce(&mut Context) -> T) -> T {
157        self.try_with_active(|context| Ok::<_, std::convert::Infallible>(f(context)))
158            .unwrap_or_else(|error| match error {
159                ScopedActivationError::Closure(never) => match never {},
160                ScopedActivationError::Scope(error) => {
161                    panic!("SuspendedContext::with_active_or_panic(): {error}")
162                }
163            })
164    }
165
166    /// Tries to create a new suspended Dear ImGui context
167    pub fn try_create() -> crate::error::ImGuiResult<Self> {
168        Self::try_create_internal(None)
169    }
170
171    /// Tries to create a new suspended Dear ImGui context with a shared font atlas.
172    ///
173    /// Multiple contexts may share the atlas while using legacy renderer-managed texture handling.
174    /// Once a managed renderer claims the atlas, registering another context returns
175    /// [`ImGuiError::SharedFontAtlasManaged`](crate::ImGuiError::SharedFontAtlasManaged).
176    /// If its prior managed Context was dropped without a committed renderer reset, this returns
177    /// [`ImGuiError::SharedFontAtlasRendererReleasePending`](crate::ImGuiError::SharedFontAtlasRendererReleasePending).
178    pub fn try_create_with_shared_font_atlas(
179        shared_font_atlas: SharedFontAtlas,
180    ) -> crate::error::ImGuiResult<Self> {
181        Self::try_create_internal(Some(shared_font_atlas))
182    }
183
184    /// Creates a new suspended Dear ImGui context (panics on error)
185    pub fn create() -> Self {
186        Self::try_create().expect("Failed to create Dear ImGui context")
187    }
188
189    /// Creates a new suspended Dear ImGui context with a shared font atlas (panics on error).
190    ///
191    /// This panics if a managed renderer has already claimed the atlas. Use
192    /// [`SuspendedContext::try_create_with_shared_font_atlas`] to handle ownership and
193    /// pending-release errors.
194    pub fn create_with_shared_font_atlas(shared_font_atlas: SharedFontAtlas) -> Self {
195        Self::try_create_with_shared_font_atlas(shared_font_atlas)
196            .expect("Failed to create Dear ImGui context")
197    }
198
199    // removed legacy create_or_panic variants (use create()/try_create())
200
201    fn try_create_internal(
202        shared_font_atlas: Option<SharedFontAtlas>,
203    ) -> crate::error::ImGuiResult<Self> {
204        if bound_context_scope_active() {
205            return Err(crate::error::ImGuiError::ContextBindingScopeActive);
206        }
207        let thread_lease = ContextThreadLease::acquire()?;
208        let _guard = CTX_MUTEX.lock();
209        let previous_context = unsafe { sys::igGetCurrentContext() };
210
211        let shared_font_atlas_ptr = match &shared_font_atlas {
212            Some(atlas) => atlas.as_ptr(),
213            None => ptr::null_mut(),
214        };
215        crate::fonts::validate_font_atlas_context_registration(shared_font_atlas_ptr)?;
216
217        let id =
218            ContextId::allocate().ok_or_else(|| crate::error::ImGuiError::ContextCreation {
219                reason: "process Context identity space is exhausted".to_string(),
220            })?;
221
222        let raw = unsafe { sys::igCreateContext(shared_font_atlas_ptr) };
223        if raw.is_null() {
224            set_current_context(previous_context);
225            return Err(crate::error::ImGuiError::ContextCreation {
226                reason: "ImGui_CreateContext returned null".to_string(),
227            });
228        }
229
230        unsafe {
231            let io = sys::igGetIO_ContextPtr(raw);
232            assert!(
233                !io.is_null(),
234                "new ImGui context returned a null IO pointer"
235            );
236            crate::fonts::register_font_atlas_context((*io).Fonts, raw);
237        }
238
239        let state = ContextState::new(id, raw);
240        let texture_registry = ManagedTextureRegistry::new(id);
241        let ui = crate::ui::Ui::new(raw, ContextBinding::new(&state), texture_registry.clone());
242
243        let ctx = Context {
244            raw,
245            state,
246            _thread_lease: thread_lease,
247            attachments: AttachmentRegistry::default(),
248            snapshot_hub: SnapshotHub::new(id),
249            texture_registry,
250            shared_font_atlas,
251            ini_filename: None,
252            log_filename: None,
253            platform_name: None,
254            renderer_name: None,
255            clipboard_ctx: Box::new(ClipboardContext::dummy()),
256            ui,
257        };
258
259        if previous_context.is_null() {
260            clear_current_context();
261        } else {
262            set_current_context(previous_context);
263        }
264
265        Ok(SuspendedContext(ctx))
266    }
267
268    /// Attempts to activate this suspended Context.
269    ///
270    /// If activation is rejected, [`ContextActivationError`] retains this suspended owner and
271    /// reports whether another Context or a binding scope blocked activation.
272    pub fn activate(self) -> Result<Context, ContextActivationError> {
273        let _guard = CTX_MUTEX.lock();
274        if bound_context_scope_active() {
275            return Err(ContextActivationError::new(
276                self,
277                ContextActivationReason::BindingScopeActive,
278            ));
279        }
280        if !no_current_context() {
281            return Err(ContextActivationError::new(
282                self,
283                ContextActivationReason::ContextAlreadyActive,
284            ));
285        }
286        set_current_context(self.0.raw);
287        Ok(self.0)
288    }
289
290    /// Activates this suspended Context or panics with the rejection reason.
291    ///
292    /// # Panics
293    ///
294    /// Panics if another Context or Context binding scope is active. Use
295    /// [`SuspendedContext::activate`] when activation conflicts are recoverable.
296    pub fn activate_or_panic(self) -> Context {
297        self.activate()
298            .unwrap_or_else(|error| panic!("SuspendedContext::activate_or_panic(): {error}"))
299    }
300}