use std::cell::RefCell;
use std::collections::HashMap;
use crate::scope::{TableScope, WindowScope};
use crate::{Ui, sys};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ClipperScope {
context: *mut sys::ImGuiContext,
window: WindowScope,
table: Option<TableScope>,
}
#[derive(Clone, Copy)]
struct ClipperEntry {
id: u64,
ptr: *mut sys::ImGuiListClipper,
scope: ClipperScope,
abandoned: bool,
}
enum ClipperCleanup {
OriginalScope(ClipperEntry),
WithoutLayout(ClipperEntry),
}
#[derive(Clone, Copy)]
pub(super) struct ClipperHandle {
id: u64,
ptr: *mut sys::ImGuiListClipper,
context: *mut sys::ImGuiContext,
}
impl ClipperHandle {
pub(super) fn ptr(self) -> *mut sys::ImGuiListClipper {
self.ptr
}
}
struct ClipperRegistry {
next_id: u64,
by_context: HashMap<usize, Vec<ClipperEntry>>,
}
impl Default for ClipperRegistry {
fn default() -> Self {
Self {
next_id: 1,
by_context: HashMap::new(),
}
}
}
thread_local! {
static CLIPPERS: RefCell<ClipperRegistry> = RefCell::new(ClipperRegistry::default());
}
unsafe fn current_scope(ui: &Ui, caller: &str) -> ClipperScope {
let context = ui.context_raw();
assert!(
!context.is_null(),
"{caller} requires a valid ImGui context"
);
assert_eq!(
unsafe { sys::igGetCurrentContext() },
context,
"{caller} requires the clipper's ImGui context to be current"
);
let scope = ui.current_native_scope();
let window = scope
.window()
.unwrap_or_else(|| panic!("{caller} requires an active ImGui window"));
ClipperScope {
context,
window,
table: scope.table(),
}
}
unsafe fn try_current_scope(ui: &Ui) -> Option<ClipperScope> {
if ui.native_scope_recovery_pending() {
return None;
}
let context = ui.context_raw();
if context.is_null() || unsafe { sys::igGetCurrentContext() } != context {
return None;
}
let scope = ui.current_native_scope();
Some(ClipperScope {
context,
window: scope.window()?,
table: scope.table(),
})
}
pub(super) unsafe fn assert_can_begin(ui: &Ui, caller: &str) {
let scope = unsafe { current_scope(ui, caller) };
let context = scope.context;
CLIPPERS.with(|registry| {
let registry = registry.borrow();
let Some(entry) = registry
.by_context
.get(&(context as usize))
.and_then(|stack| stack.last())
else {
return;
};
assert_eq!(
entry.scope, scope,
"{caller} cannot begin a nested list clipper in a different ImGui window or table scope"
);
});
}
pub(super) unsafe fn register_current(
ui: &Ui,
ptr: *mut sys::ImGuiListClipper,
caller: &str,
) -> ClipperHandle {
assert!(!ptr.is_null(), "{caller} received a null list clipper");
let scope = unsafe { current_scope(ui, caller) };
let context = scope.context;
CLIPPERS.with(|registry| {
let mut registry = registry.borrow_mut();
let id = registry.next_id;
registry.next_id = registry
.next_id
.checked_add(1)
.expect("list clipper token counter overflowed");
registry
.by_context
.entry(context as usize)
.or_default()
.push(ClipperEntry {
id,
ptr,
scope,
abandoned: false,
});
ClipperHandle { id, ptr, context }
})
}
pub(super) unsafe fn assert_current(
ui: &Ui,
handle: ClipperHandle,
caller: &str,
) -> *mut sys::ImGuiListClipper {
assert_eq!(
handle.context,
ui.context_raw(),
"{caller} used a list clipper with a foreign Ui"
);
let scope = unsafe { current_scope(ui, caller) };
CLIPPERS.with(|registry| {
let registry = registry.borrow();
let stack = registry
.by_context
.get(&(handle.context as usize))
.unwrap_or_else(|| panic!("{caller} used a list clipper that is no longer active"));
let entry = stack
.last()
.unwrap_or_else(|| panic!("{caller} used a list clipper that is no longer active"));
assert_eq!(
entry.id, handle.id,
"{caller} must follow list clipper LIFO order; a nested clipper is still active"
);
assert_eq!(
entry.ptr, handle.ptr,
"{caller} encountered an invalid list clipper registration"
);
assert!(
!entry.abandoned,
"{caller} used an abandoned list clipper token"
);
assert_eq!(
entry.scope, scope,
"{caller} must run in the exact frame, window Begin, and table instance where the clipper began"
);
entry.ptr
})
}
pub(super) unsafe fn complete(ui: &Ui, handle: ClipperHandle) {
let current_scope = unsafe { try_current_scope(ui) };
let entries_to_destroy = CLIPPERS.with(|registry| {
let mut registry = registry.borrow_mut();
let key = handle.context as usize;
let stack = registry
.by_context
.get_mut(&key)
.expect("completed list clipper was not registered");
let entry = stack
.last()
.expect("completed list clipper stack was empty");
assert_eq!(
entry.id, handle.id,
"completed list clipper must be the active native LIFO entry"
);
assert_eq!(
entry.ptr, handle.ptr,
"completed list clipper registration was invalid"
);
assert!(!entry.abandoned, "completed list clipper was abandoned");
let _completed = stack
.pop()
.expect("completed list clipper stack entry disappeared");
let mut entries = Vec::new();
while stack.last().is_some_and(|entry| entry.abandoned) {
let entry = stack.pop().expect("abandoned clipper entry disappeared");
entries.push(if current_scope == Some(entry.scope) {
ClipperCleanup::OriginalScope(entry)
} else {
ClipperCleanup::WithoutLayout(entry)
});
}
if stack.is_empty() {
registry.by_context.remove(&key);
}
entries
});
for cleanup in entries_to_destroy {
match cleanup {
ClipperCleanup::OriginalScope(entry) => unsafe { destroy_in_original_scope(entry) },
ClipperCleanup::WithoutLayout(entry) => unsafe { destroy_without_layout(entry) },
}
}
}
pub(super) unsafe fn release(ui: &Ui, handle: ClipperHandle) {
if handle.context.is_null() || handle.ptr.is_null() {
return;
}
let current_scope = unsafe { try_current_scope(ui) };
let entries_to_destroy = CLIPPERS.with(|registry| {
let mut registry = registry.borrow_mut();
let key = handle.context as usize;
let Some(stack) = registry.by_context.get_mut(&key) else {
return Vec::new();
};
let Some(position) = stack.iter().position(|entry| entry.id == handle.id) else {
return Vec::new();
};
if position + 1 != stack.len() {
stack[position].abandoned = true;
return Vec::new();
}
let entry = stack.pop().expect("clipper stack was checked as non-empty");
let mut entries = vec![if current_scope == Some(entry.scope) && !entry.abandoned {
ClipperCleanup::OriginalScope(entry)
} else {
ClipperCleanup::WithoutLayout(entry)
}];
while stack.last().is_some_and(|entry| entry.abandoned) {
let entry = stack.pop().expect("abandoned clipper entry disappeared");
entries.push(if current_scope == Some(entry.scope) {
ClipperCleanup::OriginalScope(entry)
} else {
ClipperCleanup::WithoutLayout(entry)
});
}
if stack.is_empty() {
registry.by_context.remove(&key);
}
entries
});
for cleanup in entries_to_destroy {
match cleanup {
ClipperCleanup::OriginalScope(entry) => unsafe { destroy_in_original_scope(entry) },
ClipperCleanup::WithoutLayout(entry) => unsafe { destroy_without_layout(entry) },
}
}
}
unsafe fn destroy_in_original_scope(entry: ClipperEntry) {
debug_assert_eq!(unsafe { sys::igGetCurrentContext() }, entry.scope.context);
debug_assert_eq!(unsafe { (*entry.ptr).Ctx }, entry.scope.context);
unsafe { sys::ImGuiListClipper_destroy(entry.ptr) };
}
unsafe fn destroy_without_layout(entry: ClipperEntry) {
let context = entry.scope.context;
debug_assert!(!context.is_null());
debug_assert_eq!(unsafe { (*entry.ptr).Ctx }, context);
unsafe {
(*entry.ptr).ItemsCount = -1;
sys::ImGuiListClipper_End(entry.ptr);
sys::ImGuiListClipper_destroy(entry.ptr);
}
}
pub(super) unsafe fn forget_context(context: *mut sys::ImGuiContext) -> usize {
let entries = CLIPPERS.with(|registry| {
registry
.borrow_mut()
.by_context
.remove(&(context as usize))
.unwrap_or_default()
});
let abandoned = entries.len();
for entry in entries.into_iter().rev() {
unsafe { destroy_without_layout(entry) };
}
abandoned
}
#[cfg(test)]
pub(super) fn active_count(context: *mut sys::ImGuiContext) -> usize {
CLIPPERS.with(|registry| {
registry
.borrow()
.by_context
.get(&(context as usize))
.map_or(0, Vec::len)
})
}