use std::ops::Range;
use crate::Ui;
use crate::sys;
fn items_count_to_i32(items_count: usize, caller: &str) -> i32 {
i32::try_from(items_count)
.unwrap_or_else(|_| panic!("{caller} items_count exceeded Dear ImGui's i32 range"))
}
fn display_index_from_i32(index: i32, caller: &str) -> usize {
assert!(index >= 0, "{caller} returned a negative display index");
usize::try_from(index).expect("non-negative display index must fit usize")
}
pub struct ListClipper {
items_count: usize,
items_height: f32,
}
impl ListClipper {
pub const fn new(items_count: usize) -> Self {
ListClipper {
items_count,
items_height: -1.0,
}
}
pub const fn items_height(mut self, items_height: f32) -> Self {
self.items_height = items_height;
self
}
pub fn begin(self, ui: &Ui) -> ListClipperToken<'_> {
assert!(
self.items_height.is_finite(),
"ListClipper::begin() items_height must be finite"
);
let items_count = items_count_to_i32(self.items_count, "ListClipper::begin()");
ui.run_with_bound_context(|| unsafe {
let ptr = sys::ImGuiListClipper_ImGuiListClipper();
if ptr.is_null() {
panic!("ImGuiListClipper_ImGuiListClipper() returned null");
}
sys::ImGuiListClipper_Begin(ptr, items_count, self.items_height);
ListClipperToken::new(ui, ptr)
})
}
}
pub struct ListClipperToken<'ui> {
ui: &'ui Ui,
list_clipper: *mut sys::ImGuiListClipper,
ended: bool,
}
impl<'ui> ListClipperToken<'ui> {
fn new(ui: &'ui Ui, list_clipper: *mut sys::ImGuiListClipper) -> Self {
Self {
ui,
list_clipper,
ended: false,
}
}
pub fn step(&mut self) -> bool {
if self.ended {
panic!("ListClipperToken::step() called after the clipper has ended");
}
let ret = self
.ui
.run_with_bound_context(|| unsafe { sys::ImGuiListClipper_Step(self.list_clipper) });
if !ret {
self.ended = true;
}
ret
}
pub fn end(&mut self) {
if !self.ended {
self.ui.run_with_bound_context(|| unsafe {
sys::ImGuiListClipper_End(self.list_clipper);
});
self.ended = true;
}
}
pub fn display_start(&self) -> usize {
display_index_from_i32(
unsafe { (*self.list_clipper).DisplayStart },
"ListClipperToken::display_start()",
)
}
pub fn display_end(&self) -> usize {
display_index_from_i32(
unsafe { (*self.list_clipper).DisplayEnd },
"ListClipperToken::display_end()",
)
}
pub fn display_range(&self) -> Range<usize> {
self.display_start()..self.display_end()
}
pub fn iter(self) -> ListClipperIterator<'ui> {
ListClipperIterator::new(self)
}
}
impl Drop for ListClipperToken<'_> {
fn drop(&mut self) {
self.ui.run_with_bound_context(|| unsafe {
sys::ImGuiListClipper_destroy(self.list_clipper);
});
}
}
#[cfg(test)]
mod tests {
use super::*;
fn setup_context() -> crate::Context {
let mut ctx = crate::Context::create();
let _ = ctx.font_atlas_mut().build();
ctx.io_mut().set_display_size([128.0, 128.0]);
ctx.io_mut().set_delta_time(1.0 / 60.0);
ctx
}
#[test]
fn step_after_end_panics_before_ffi() {
let mut ctx = setup_context();
let ui = ctx.frame();
ui.window("list_clipper_step_after_end").build(|| {
let mut clipper = ListClipper::new(0).begin(ui);
clipper.end();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = clipper.step();
}));
assert!(result.is_err());
});
}
#[test]
fn end_after_step_false_is_a_noop() {
let mut ctx = setup_context();
let ui = ctx.frame();
ui.window("list_clipper_end_after_step_false").build(|| {
let mut clipper = ListClipper::new(0).begin(ui);
assert!(!clipper.step());
clipper.end();
});
}
#[test]
fn begin_rejects_invalid_inputs_before_ffi() {
let mut ctx = setup_context();
let ui = ctx.frame();
ui.window("list_clipper_invalid_inputs").build(|| {
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _clipper = ListClipper::new(usize::MAX).begin(ui);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _clipper = ListClipper::new(1usize).items_height(f32::NAN).begin(ui);
}))
.is_err()
);
});
}
#[test]
fn iterator_and_display_range_use_usize_indices() {
let mut ctx = setup_context();
ctx.io_mut().set_display_size([512.0, 512.0]);
let ui = ctx.frame();
ui.window("list_clipper_usize_indices")
.size([256.0, 256.0], crate::Condition::Always)
.build(|| {
let mut clipper = ListClipper::new(3usize).items_height(1.0).begin(ui);
while clipper.step() {
for index in clipper.display_range() {
let _: usize = index;
ui.text(format!("row {index}"));
}
}
let indices: Vec<usize> = ListClipper::new(3usize)
.items_height(1.0)
.begin(ui)
.iter()
.inspect(|index| ui.text(format!("row {index}")))
.collect();
assert_eq!(indices, vec![0, 1, 2]);
});
}
}
pub struct ListClipperIterator<'ui> {
list_clipper: ListClipperToken<'ui>,
exhausted: bool,
last_value: Option<usize>,
}
impl<'ui> ListClipperIterator<'ui> {
fn new(list_clipper: ListClipperToken<'ui>) -> Self {
Self {
list_clipper,
exhausted: false,
last_value: None,
}
}
}
impl Iterator for ListClipperIterator<'_> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some(value) = self.last_value {
let next_value = value + 1;
if next_value >= self.list_clipper.display_end() {
self.last_value = None;
} else {
self.last_value = Some(next_value);
}
return Some(value);
}
if self.exhausted {
return None;
}
let ret = self.list_clipper.step();
if !ret {
self.exhausted = true;
return None;
}
let start = self.list_clipper.display_start();
let end = self.list_clipper.display_end();
if start < end {
let next_value = start + 1;
if next_value < end {
self.last_value = Some(next_value);
}
return Some(start);
} else {
self.last_value = None;
}
}
}
}