1use std::cell::{Cell, RefCell};
2use std::collections::{HashMap, HashSet};
3use std::fmt;
4use std::num::NonZeroU64;
5use std::ptr;
6use std::rc::{Rc, Weak};
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::thread::ThreadId;
9
10use parking_lot::{Mutex, ReentrantMutex};
11use thiserror::Error;
12
13use crate::sys;
14
15pub(crate) static CTX_MUTEX: ReentrantMutex<()> = parking_lot::const_reentrant_mutex(());
17
18static CONTEXT_THREAD_OWNER: Mutex<ContextThreadOwner> =
19 parking_lot::const_mutex(ContextThreadOwner::new());
20
21static NEXT_CONTEXT_ID: AtomicU64 = AtomicU64::new(1);
22
23thread_local! {
24 static MANAGED_CONTEXTS: RefCell<HashMap<usize, ManagedContextEntry>> =
27 RefCell::new(HashMap::new());
28 static LIVE_MAIN_VIEWPORTS: RefCell<HashMap<usize, ContextId>> =
32 RefCell::new(HashMap::new());
33 static BOUND_CONTEXT_DEPTH: Cell<usize> = const { Cell::new(0) };
34}
35
36#[derive(Debug)]
37struct ContextThreadOwner {
38 thread: Option<ThreadId>,
39 live_contexts: usize,
40}
41
42impl ContextThreadOwner {
43 const fn new() -> Self {
44 Self {
45 thread: None,
46 live_contexts: 0,
47 }
48 }
49}
50
51#[derive(Debug)]
53pub(crate) struct ContextThreadLease {
54 thread: ThreadId,
55}
56
57impl ContextThreadLease {
58 pub(crate) fn acquire() -> crate::error::ImGuiResult<Self> {
59 let thread = std::thread::current().id();
60 let mut owner = CONTEXT_THREAD_OWNER.lock();
61 if owner.thread.is_some_and(|current| current != thread) {
62 return Err(crate::error::ImGuiError::ContextThreadConflict);
63 }
64 owner.live_contexts = owner.live_contexts.checked_add(1).ok_or_else(|| {
65 crate::error::ImGuiError::context_creation(
66 "process Context ownership count is exhausted",
67 )
68 })?;
69 owner.thread = Some(thread);
70 Ok(Self { thread })
71 }
72}
73
74impl Drop for ContextThreadLease {
75 fn drop(&mut self) {
76 let mut owner = CONTEXT_THREAD_OWNER.lock();
77 debug_assert_eq!(owner.thread, Some(self.thread));
78 debug_assert!(owner.live_contexts > 0);
79 owner.live_contexts -= 1;
80 if owner.live_contexts == 0 {
81 owner.thread = None;
82 }
83 }
84}
85
86pub(super) fn bound_context_scope_active() -> bool {
87 BOUND_CONTEXT_DEPTH.with(|depth| depth.get() != 0)
88}
89
90pub(crate) fn viewport_is_main_viewport(viewport: *const sys::ImGuiViewport) -> bool {
96 if viewport.is_null() {
97 return false;
98 }
99
100 if LIVE_MAIN_VIEWPORTS.with(|viewports| viewports.borrow().contains_key(&(viewport as usize))) {
101 return true;
102 }
103
104 let _lock = CTX_MUTEX.lock();
107 let current = unsafe { sys::igGetCurrentContext() };
108 if current.is_null() {
109 return false;
110 }
111 let current_is_managed = MANAGED_CONTEXTS.with(|contexts| {
112 matches!(
113 contexts.borrow().get(&(current as usize)),
114 Some(ManagedContextEntry::Live { state, .. })
115 if state.upgrade().is_some_and(|state| {
116 state.lifecycle() != ContextLifecycle::NativeDestroyed
117 && state.raw_during_teardown() == current
118 })
119 )
120 });
121 if current_is_managed {
122 return false;
123 }
124
125 let main = unsafe { sys::igGetMainViewport() };
126 !main.is_null() && std::ptr::eq(viewport, main.cast_const())
127}
128
129#[derive(Clone)]
130enum ManagedContextEntry {
131 Live {
132 id: ContextId,
133 state: Weak<ContextState>,
134 },
135 Dead {
136 id: ContextId,
137 },
138}
139
140#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
142pub struct ContextId(NonZeroU64);
143
144impl ContextId {
145 pub(crate) fn allocate() -> Option<Self> {
146 let value = NEXT_CONTEXT_ID
147 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
148 current.checked_add(1)
149 })
150 .ok()?;
151 NonZeroU64::new(value).map(Self)
152 }
153
154 pub fn get(self) -> NonZeroU64 {
156 self.0
157 }
158}
159
160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162#[non_exhaustive]
163pub enum ContextLifecycle {
164 Alive,
166 Dropping,
168 NativeDestroyed,
170}
171
172pub(crate) struct ContextState {
173 id: ContextId,
174 address: usize,
175 main_viewport_address: usize,
176 raw: Cell<*mut sys::ImGuiContext>,
177 lifecycle: Cell<ContextLifecycle>,
178 dockspace_submissions: RefCell<FrameIdClaims>,
179 dock_layout_applications: RefCell<FrameIdClaims>,
180}
181
182#[derive(Default)]
183struct FrameIdClaims {
184 frame: Option<i32>,
185 ids: HashSet<sys::ImGuiID>,
186}
187
188impl FrameIdClaims {
189 fn claim(&mut self, frame: i32, id: sys::ImGuiID) -> bool {
190 if self.frame != Some(frame) {
191 self.frame = Some(frame);
192 self.ids.clear();
193 }
194 self.ids.insert(id)
195 }
196
197 fn release(&mut self, frame: i32, id: sys::ImGuiID) {
198 if self.frame == Some(frame) {
199 self.ids.remove(&id);
200 }
201 }
202}
203
204impl fmt::Debug for ContextState {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 f.debug_struct("ContextState")
207 .field("id", &self.id)
208 .field("raw", &self.raw.get())
209 .field("lifecycle", &self.lifecycle.get())
210 .finish()
211 }
212}
213
214impl ContextState {
215 pub(crate) fn new(id: ContextId, raw: *mut sys::ImGuiContext) -> Rc<Self> {
216 let main_viewport = with_bound_context(raw, || unsafe { sys::igGetMainViewport() });
217 assert!(
218 !main_viewport.is_null(),
219 "new ImGui context returned a null main viewport"
220 );
221 let main_viewport_address = main_viewport as usize;
222 let state = Rc::new(Self {
223 id,
224 address: raw as usize,
225 main_viewport_address,
226 raw: Cell::new(raw),
227 lifecycle: Cell::new(ContextLifecycle::Alive),
228 dockspace_submissions: RefCell::new(FrameIdClaims::default()),
229 dock_layout_applications: RefCell::new(FrameIdClaims::default()),
230 });
231 MANAGED_CONTEXTS.with(|contexts| {
232 contexts.borrow_mut().insert(
233 raw as usize,
234 ManagedContextEntry::Live {
235 id,
236 state: Rc::downgrade(&state),
237 },
238 );
239 });
240 LIVE_MAIN_VIEWPORTS.with(|viewports| {
241 let mut viewports = viewports.borrow_mut();
242 debug_assert!(
243 !viewports.contains_key(&main_viewport_address),
244 "two live ImGui contexts exposed the same main viewport address"
245 );
246 viewports.insert(main_viewport_address, id);
247 });
248 state
249 }
250
251 pub(crate) fn id(&self) -> ContextId {
252 self.id
253 }
254
255 pub(crate) fn lifecycle(&self) -> ContextLifecycle {
256 self.lifecycle.get()
257 }
258
259 pub(crate) fn raw_during_teardown(&self) -> *mut sys::ImGuiContext {
260 self.raw.get()
261 }
262
263 pub(crate) fn begin_drop(&self) {
264 debug_assert_eq!(self.lifecycle.get(), ContextLifecycle::Alive);
265 self.lifecycle.set(ContextLifecycle::Dropping);
266 }
267
268 pub(crate) fn mark_native_destroyed(&self) {
269 self.unregister_main_viewport();
270 self.raw.set(ptr::null_mut());
271 self.lifecycle.set(ContextLifecycle::NativeDestroyed);
272 }
273
274 fn unregister_main_viewport(&self) {
275 let _ = LIVE_MAIN_VIEWPORTS.try_with(|viewports| {
276 let mut viewports = viewports.borrow_mut();
277 if viewports.get(&self.main_viewport_address) == Some(&self.id) {
278 viewports.remove(&self.main_viewport_address);
279 }
280 });
281 }
282}
283
284impl Drop for ContextState {
285 fn drop(&mut self) {
286 self.unregister_main_viewport();
287 let _ = MANAGED_CONTEXTS.try_with(|contexts| {
288 let mut contexts = contexts.borrow_mut();
289 let Some(entry) = contexts.get_mut(&self.address) else {
290 return;
291 };
292 let matches_generation = match entry {
293 ManagedContextEntry::Live { id, .. } | ManagedContextEntry::Dead { id } => {
294 *id == self.id
295 }
296 };
297 if matches_generation {
298 *entry = ManagedContextEntry::Dead { id: self.id };
299 }
300 });
301 }
302}
303
304#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
306#[non_exhaustive]
307pub enum ContextBindingError {
308 #[error("Dear ImGui context teardown is in progress")]
310 Dropping,
311 #[error("Dear ImGui context has been destroyed")]
313 NativeDestroyed,
314}
315
316#[derive(Clone)]
318#[must_use]
319pub struct ContextBinding {
320 state: Weak<ContextState>,
321 id: ContextId,
322}
323
324impl fmt::Debug for ContextBinding {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 f.debug_struct("ContextBinding")
327 .field("id", &self.id)
328 .field("lifecycle", &self.lifecycle())
329 .finish()
330 }
331}
332
333impl ContextBinding {
334 pub(crate) fn new(state: &Rc<ContextState>) -> Self {
335 Self {
336 state: Rc::downgrade(state),
337 id: state.id(),
338 }
339 }
340
341 pub fn id(&self) -> ContextId {
343 self.id
344 }
345
346 pub fn lifecycle(&self) -> ContextLifecycle {
348 self.state
349 .upgrade()
350 .map_or(ContextLifecycle::NativeDestroyed, |state| state.lifecycle())
351 }
352
353 pub fn is_alive(&self) -> bool {
355 self.lifecycle() == ContextLifecycle::Alive
356 }
357
358 pub(crate) fn claim_dockspace_submission(
359 &self,
360 frame: i32,
361 id: sys::ImGuiID,
362 ) -> Option<DockspaceFrameClaim> {
363 self.claim_dockspace_frame_id(frame, id, DockspaceClaimKind::Submission)
364 }
365
366 pub(crate) fn claim_dock_layout_application(
367 &self,
368 frame: i32,
369 id: sys::ImGuiID,
370 ) -> Option<DockspaceFrameClaim> {
371 self.claim_dockspace_frame_id(frame, id, DockspaceClaimKind::LayoutApplication)
372 }
373
374 fn claim_dockspace_frame_id(
375 &self,
376 frame: i32,
377 id: sys::ImGuiID,
378 kind: DockspaceClaimKind,
379 ) -> Option<DockspaceFrameClaim> {
380 let state = self.state.upgrade()?;
381 if state.lifecycle() != ContextLifecycle::Alive {
382 return None;
383 }
384
385 let claimed = match kind {
386 DockspaceClaimKind::Submission => {
387 state.dockspace_submissions.borrow_mut().claim(frame, id)
388 }
389 DockspaceClaimKind::LayoutApplication => {
390 state.dock_layout_applications.borrow_mut().claim(frame, id)
391 }
392 };
393 if !claimed {
394 return None;
395 }
396
397 Some(DockspaceFrameClaim {
398 state: Rc::downgrade(&state),
399 frame,
400 id,
401 kind,
402 committed: false,
403 })
404 }
405
406 pub fn try_with_bound_context<R>(
408 &self,
409 f: impl FnOnce() -> R,
410 ) -> Result<R, ContextBindingError> {
411 self.try_with_bound_context_guarded(|_| f())
412 }
413
414 pub(crate) fn try_with_bound_context_guarded<R>(
415 &self,
416 f: impl FnOnce(&mut RawBoundContextGuard) -> R,
417 ) -> Result<R, ContextBindingError> {
418 let state = self
419 .state
420 .upgrade()
421 .ok_or(ContextBindingError::NativeDestroyed)?;
422 match state.lifecycle() {
423 ContextLifecycle::Alive => {}
424 ContextLifecycle::Dropping => return Err(ContextBindingError::Dropping),
425 ContextLifecycle::NativeDestroyed => {
426 return Err(ContextBindingError::NativeDestroyed);
427 }
428 }
429
430 let _lock = CTX_MUTEX.lock();
431 match state.lifecycle() {
432 ContextLifecycle::Alive => {}
433 ContextLifecycle::Dropping => return Err(ContextBindingError::Dropping),
434 ContextLifecycle::NativeDestroyed => {
435 return Err(ContextBindingError::NativeDestroyed);
436 }
437 }
438 let raw = state.raw.get();
439 if raw.is_null() {
440 return Err(ContextBindingError::NativeDestroyed);
441 }
442
443 let mut bound = RawBoundContextGuard::bind(raw);
444 Ok(f(&mut bound))
445 }
446
447 pub fn with_bound_context<R>(&self, f: impl FnOnce() -> R) -> R {
454 self.try_with_bound_context(f)
455 .unwrap_or_else(|error| panic!("ContextBinding::with_bound_context(): {error}"))
456 }
457}
458
459#[derive(Clone, Copy)]
460enum DockspaceClaimKind {
461 Submission,
462 LayoutApplication,
463}
464
465pub(crate) struct DockspaceFrameClaim {
466 state: Weak<ContextState>,
467 frame: i32,
468 id: sys::ImGuiID,
469 kind: DockspaceClaimKind,
470 committed: bool,
471}
472
473impl DockspaceFrameClaim {
474 pub(crate) fn commit(mut self) {
475 self.committed = true;
476 }
477}
478
479impl Drop for DockspaceFrameClaim {
480 fn drop(&mut self) {
481 if self.committed {
482 return;
483 }
484 let Some(state) = self.state.upgrade() else {
485 return;
486 };
487 match self.kind {
488 DockspaceClaimKind::Submission => state
489 .dockspace_submissions
490 .borrow_mut()
491 .release(self.frame, self.id),
492 DockspaceClaimKind::LayoutApplication => state
493 .dock_layout_applications
494 .borrow_mut()
495 .release(self.frame, self.id),
496 }
497 }
498}
499
500#[derive(Clone, Debug)]
502#[must_use]
503pub struct ContextAliveToken(ContextBinding);
504
505impl ContextAliveToken {
506 pub(crate) fn from_binding(binding: ContextBinding) -> Self {
507 Self(binding)
508 }
509
510 pub fn is_alive(&self) -> bool {
512 self.0.is_alive()
513 }
514}
515
516pub(crate) struct RawBoundContextGuard {
517 previous: *mut sys::ImGuiContext,
518 previous_state: Option<ManagedContextEntry>,
519 restore: bool,
520}
521
522impl RawBoundContextGuard {
523 pub(crate) fn bind(target: *mut sys::ImGuiContext) -> Self {
524 BOUND_CONTEXT_DEPTH.with(|depth| {
525 depth.set(
526 depth
527 .get()
528 .checked_add(1)
529 .expect("Dear ImGui Context binding depth overflowed"),
530 );
531 });
532 unsafe {
533 let previous = sys::igGetCurrentContext();
534 let restore = previous != target;
535 let previous_state = if restore {
536 MANAGED_CONTEXTS
537 .try_with(|contexts| contexts.borrow().get(&(previous as usize)).cloned())
538 .ok()
539 .flatten()
540 } else {
541 None
542 };
543 if restore {
544 sys::igSetCurrentContext(target);
545 }
546 Self {
547 previous,
548 previous_state,
549 restore,
550 }
551 }
552 }
553
554 pub(crate) fn previous_context(&self) -> *mut sys::ImGuiContext {
555 self.previous
556 }
557}
558
559impl Drop for RawBoundContextGuard {
560 fn drop(&mut self) {
561 if self.restore {
562 let previous_is_valid = match self.previous_state.as_ref() {
563 None => true,
564 Some(ManagedContextEntry::Live { id, state }) => {
565 state.upgrade().is_some_and(|state| {
566 state.id() == *id
567 && state.lifecycle() != ContextLifecycle::NativeDestroyed
568 && state.raw_during_teardown() == self.previous
569 })
570 }
571 Some(ManagedContextEntry::Dead { .. }) => false,
572 };
573 set_current_context(if previous_is_valid {
574 self.previous
575 } else {
576 ptr::null_mut()
577 });
578 }
579 BOUND_CONTEXT_DEPTH.with(|depth| {
580 let current = depth.get();
581 debug_assert!(current > 0);
582 depth.set(current - 1);
583 });
584 }
585}
586
587pub(super) fn clear_current_context() {
588 set_current_context(ptr::null_mut());
589}
590
591pub(super) fn set_current_context(ctx: *mut sys::ImGuiContext) {
592 unsafe { sys::igSetCurrentContext(ctx) }
593}
594
595pub(super) fn no_current_context() -> bool {
596 let ctx = unsafe { sys::igGetCurrentContext() };
597 ctx.is_null()
598}
599
600pub(crate) fn with_bound_context<R>(ctx: *mut sys::ImGuiContext, f: impl FnOnce() -> R) -> R {
601 let _lock = CTX_MUTEX.lock();
602 let _bound = RawBoundContextGuard::bind(ctx);
603 f()
604}