1use std::ops::Range;
7
8use crate::Ui;
9use crate::sys;
10
11mod registry;
12
13use registry::ClipperHandle;
14
15fn items_count_to_i32(items_count: usize, caller: &str) -> i32 {
16 i32::try_from(items_count)
17 .unwrap_or_else(|_| panic!("{caller} items_count exceeded Dear ImGui's i32 range"))
18}
19
20fn validate_items_height(items_height: f32, caller: &str) {
21 assert!(
22 items_height.is_finite(),
23 "{caller} items_height must be finite"
24 );
25 assert!(
26 items_height == -1.0 || items_height > 0.0,
27 "{caller} items_height must be -1.0 for automatic measurement or a positive value"
28 );
29}
30
31fn final_unknown_count_to_i32(items_count: usize, caller: &str) -> i32 {
32 assert!(
33 items_count <= i32::MAX as usize,
34 "{caller} final_items_count exceeded Dear ImGui's i32 range"
35 );
36 items_count as i32
37}
38
39fn display_index_from_i32(index: i32, caller: &str) -> usize {
40 assert!(index >= 0, "{caller} returned a negative display index");
41 usize::try_from(index).expect("non-negative display index must fit usize")
42}
43
44pub(crate) unsafe fn forget_context_clippers(context: *mut sys::ImGuiContext) -> usize {
45 unsafe { registry::forget_context(context) }
46}
47
48pub struct ListClipper {
61 items_count: usize,
62 items_height: f32,
63}
64
65impl ListClipper {
66 pub const fn new(items_count: usize) -> Self {
68 ListClipper {
69 items_count,
70 items_height: -1.0,
71 }
72 }
73
74 pub const fn unknown_count() -> UnknownCountListClipper {
76 UnknownCountListClipper { items_height: -1.0 }
77 }
78
79 pub const fn items_height(mut self, items_height: f32) -> Self {
81 self.items_height = items_height;
82 self
83 }
84
85 pub fn begin(self, ui: &Ui) -> ListClipperToken<'_> {
86 assert!(
87 self.items_count < i32::MAX as usize,
88 "ListClipper::begin() known items_count must be less than i32::MAX; use ListClipper::unknown_count() for the sentinel protocol"
89 );
90 validate_items_height(self.items_height, "ListClipper::begin()");
91 let items_count = items_count_to_i32(self.items_count, "ListClipper::begin()");
92 ListClipperToken::new(
93 ActiveListClipper::begin(ui, items_count, self.items_height, "ListClipper::begin()"),
94 self.items_count,
95 )
96 }
97}
98
99pub struct UnknownCountListClipper {
101 items_height: f32,
102}
103
104impl UnknownCountListClipper {
105 pub const fn items_height(mut self, items_height: f32) -> Self {
107 self.items_height = items_height;
108 self
109 }
110
111 pub fn begin(self, ui: &Ui) -> UnknownCountListClipperToken<'_> {
113 validate_items_height(self.items_height, "UnknownCountListClipper::begin()");
114 UnknownCountListClipperToken {
115 active: ActiveListClipper::begin(
116 ui,
117 i32::MAX,
118 self.items_height,
119 "UnknownCountListClipper::begin()",
120 ),
121 }
122 }
123}
124
125struct ActiveListClipper<'ui> {
126 ui: &'ui Ui,
127 handle: ClipperHandle,
128 stepped: bool,
129 ended: bool,
130 registered: bool,
131 retain_registration_after_exhaustion: bool,
132}
133
134impl<'ui> ActiveListClipper<'ui> {
135 fn begin(ui: &'ui Ui, items_count: i32, items_height: f32, caller: &str) -> Self {
136 ui.run_with_bound_context(|| unsafe {
137 registry::assert_can_begin(ui, caller);
138 let ptr = sys::ImGuiListClipper_ImGuiListClipper();
139 if ptr.is_null() {
140 panic!("ImGuiListClipper_ImGuiListClipper() returned null");
141 }
142 sys::ImGuiListClipper_Begin(ptr, items_count, items_height);
143 let handle = registry::register_current(ui, ptr, caller);
144 Self {
145 ui,
146 handle,
147 stepped: false,
148 ended: false,
149 registered: true,
150 retain_registration_after_exhaustion: items_count == i32::MAX,
151 }
152 })
153 }
154
155 fn include_items_by_index(&mut self, item_begin: i32, item_end: i32, caller: &str) {
156 self.ui.run_with_bound_context(|| unsafe {
157 let ptr = registry::assert_current(self.ui, self.handle, caller);
158 sys::ImGuiListClipper_IncludeItemsByIndex(ptr, item_begin, item_end);
159 });
160 }
161
162 fn step(&mut self, caller: &str) -> bool {
163 self.stepped = true;
164 let has_range = self.ui.run_with_bound_context(|| unsafe {
165 let ptr = registry::assert_current(self.ui, self.handle, caller);
166 sys::ImGuiListClipper_Step(ptr)
167 });
168 if !has_range {
169 self.ended = true;
170 if !self.retain_registration_after_exhaustion {
171 self.ui.run_with_bound_context(|| unsafe {
172 registry::complete(self.ui, self.handle);
173 });
174 self.registered = false;
175 }
176 }
177 has_range
178 }
179
180 fn end(&mut self, caller: &str) {
181 if !self.ended {
182 self.ui.run_with_bound_context(|| unsafe {
183 let ptr = registry::assert_current(self.ui, self.handle, caller);
184 sys::ImGuiListClipper_End(ptr);
185 });
186 self.ended = true;
187 self.ui.run_with_bound_context(|| unsafe {
188 registry::complete(self.ui, self.handle);
189 });
190 self.registered = false;
191 }
192 }
193
194 fn display_start(&self, caller: &str) -> usize {
195 self.ui.run_with_bound_context(|| unsafe {
196 let ptr = registry::assert_current(self.ui, self.handle, caller);
197 display_index_from_i32((*ptr).DisplayStart, caller)
198 })
199 }
200
201 fn display_end(&self, caller: &str) -> usize {
202 self.ui.run_with_bound_context(|| unsafe {
203 let ptr = registry::assert_current(self.ui, self.handle, caller);
204 display_index_from_i32((*ptr).DisplayEnd, caller)
205 })
206 }
207}
208
209impl Drop for ActiveListClipper<'_> {
210 fn drop(&mut self) {
211 let binding = self.ui.ctx_binding.clone();
212 let _ = binding.try_with_bound_context(|| unsafe {
213 if self.registered {
214 registry::release(self.ui, self.handle);
215 } else {
216 sys::ImGuiListClipper_destroy(self.handle.ptr());
217 }
218 });
219 }
220}
221
222pub struct ListClipperToken<'ui> {
232 active: ActiveListClipper<'ui>,
233 items_count: usize,
234}
235
236impl<'ui> ListClipperToken<'ui> {
237 fn new(active: ActiveListClipper<'ui>, items_count: usize) -> Self {
238 Self {
239 active,
240 items_count,
241 }
242 }
243
244 #[doc(alias = "IncludeItemByIndex")]
248 pub fn include_item_by_index(&mut self, item_index: usize) {
249 let item_end = item_index
250 .checked_add(1)
251 .expect("ListClipperToken::include_item_by_index() index overflowed");
252 self.include_items_by_index(item_index..item_end);
253 }
254
255 #[doc(alias = "IncludeItemsByIndex")]
259 pub fn include_items_by_index(&mut self, items: Range<usize>) {
260 assert!(
261 !self.active.ended && !self.active.stepped,
262 "ListClipperToken::include_items_by_index() must be called before the first step()"
263 );
264 assert!(
265 items.start <= items.end,
266 "ListClipperToken::include_items_by_index() range start must not exceed its end"
267 );
268 assert!(
269 items.end <= self.items_count,
270 "ListClipperToken::include_items_by_index() range exceeds the item count"
271 );
272 if items.is_empty() {
273 return;
274 }
275 let item_begin = items_count_to_i32(
276 items.start,
277 "ListClipperToken::include_items_by_index() range start",
278 );
279 let item_end = items_count_to_i32(
280 items.end,
281 "ListClipperToken::include_items_by_index() range end",
282 );
283 self.active.include_items_by_index(
284 item_begin,
285 item_end,
286 "ListClipperToken::include_items_by_index()",
287 );
288 }
289
290 pub fn step(&mut self) -> bool {
301 if self.active.ended {
302 panic!("ListClipperToken::step() called after the clipper has ended");
303 }
304 self.active.step("ListClipperToken::step()")
305 }
306
307 pub fn end(&mut self) {
310 self.active.end("ListClipperToken::end()");
311 }
312
313 pub fn display_start(&self) -> usize {
315 self.active
316 .display_start("ListClipperToken::display_start()")
317 }
318
319 pub fn display_end(&self) -> usize {
321 self.active.display_end("ListClipperToken::display_end()")
322 }
323
324 pub fn display_range(&self) -> Range<usize> {
326 self.display_start()..self.display_end()
327 }
328
329 pub fn iter(self) -> ListClipperIterator<'ui> {
332 ListClipperIterator::new(self)
333 }
334}
335
336#[must_use = "call finish(final_items_count) to finalize an unknown-count list"]
342pub struct UnknownCountListClipperToken<'ui> {
343 active: ActiveListClipper<'ui>,
344}
345
346impl UnknownCountListClipperToken<'_> {
347 #[doc(alias = "IncludeItemByIndex")]
349 pub fn include_item_by_index(&mut self, item_index: usize) {
350 let item_end = item_index
351 .checked_add(1)
352 .expect("UnknownCountListClipperToken::include_item_by_index() index overflowed");
353 self.include_items_by_index(item_index..item_end);
354 }
355
356 #[doc(alias = "IncludeItemsByIndex")]
358 pub fn include_items_by_index(&mut self, items: Range<usize>) {
359 assert!(
360 !self.active.ended && !self.active.stepped,
361 "UnknownCountListClipperToken::include_items_by_index() must be called before the first next_range()"
362 );
363 assert!(
364 items.start <= items.end,
365 "UnknownCountListClipperToken::include_items_by_index() range start must not exceed its end"
366 );
367 assert!(
368 items.end <= i32::MAX as usize,
369 "UnknownCountListClipperToken::include_items_by_index() range exceeded Dear ImGui's i32 range"
370 );
371 if items.is_empty() {
372 return;
373 }
374 let start = items_count_to_i32(
375 items.start,
376 "UnknownCountListClipperToken::include_items_by_index() range start",
377 );
378 let end = items_count_to_i32(
379 items.end,
380 "UnknownCountListClipperToken::include_items_by_index() range end",
381 );
382 self.active.include_items_by_index(
383 start,
384 end,
385 "UnknownCountListClipperToken::include_items_by_index()",
386 );
387 }
388
389 pub fn next_range(&mut self) -> Option<Range<usize>> {
393 if self.active.ended {
394 return None;
395 }
396 let has_range = self
397 .active
398 .step("UnknownCountListClipperToken::next_range()");
399 if !has_range {
400 return None;
401 }
402 Some(
403 self.active
404 .display_start("UnknownCountListClipperToken::next_range() start")
405 ..self
406 .active
407 .display_end("UnknownCountListClipperToken::next_range() end"),
408 )
409 }
410
411 #[doc(alias = "SeekCursorForItem")]
417 pub fn finish(mut self, final_items_count: usize) {
418 let final_items_count =
419 final_unknown_count_to_i32(final_items_count, "UnknownCountListClipperToken::finish()");
420
421 let items_height = self.active.ui.run_with_bound_context(|| unsafe {
422 let ptr = registry::assert_current(
423 self.active.ui,
424 self.active.handle,
425 "UnknownCountListClipperToken::finish()",
426 );
427 (*ptr).ItemsHeight
428 });
429 if final_items_count == 0 && !(items_height.is_finite() && items_height > 0.0) {
430 self.active.end("UnknownCountListClipperToken::finish()");
431 return;
432 }
433 assert!(
434 items_height.is_finite() && items_height > 0.0,
435 "UnknownCountListClipperToken::finish() could not determine a positive item height"
436 );
437 self.active.ui.run_with_bound_context(|| unsafe {
438 let ptr = registry::assert_current(
439 self.active.ui,
440 self.active.handle,
441 "UnknownCountListClipperToken::finish()",
442 );
443 sys::ImGuiListClipper_SeekCursorForItem(ptr, final_items_count);
444 });
445 self.active.end("UnknownCountListClipperToken::finish()");
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452
453 fn setup_context() -> crate::Context {
454 let mut ctx = crate::Context::create();
455 ctx.font_atlas()
456 .try_claim_legacy_renderer()
457 .expect("legacy renderer font atlas should be available")
458 .build();
459 ctx.io_mut().set_display_size([128.0, 128.0]);
460 ctx.io_mut().set_delta_time(1.0 / 60.0);
461 ctx
462 }
463
464 #[test]
465 fn step_after_end_panics_before_ffi() {
466 let mut ctx = setup_context();
467 let ui = ctx.frame();
468
469 ui.window("list_clipper_step_after_end").build(|| {
470 let mut clipper = ListClipper::new(0).begin(ui);
471 clipper.end();
472
473 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
474 let _ = clipper.step();
475 }));
476
477 assert!(result.is_err());
478 });
479 }
480
481 #[test]
482 fn end_after_step_false_is_a_noop() {
483 let mut ctx = setup_context();
484 let ui = ctx.frame();
485
486 ui.window("list_clipper_end_after_step_false").build(|| {
487 let mut clipper = ListClipper::new(0).begin(ui);
488 assert!(!clipper.step());
489 clipper.end();
490 });
491 }
492
493 #[test]
494 fn begin_rejects_invalid_inputs_before_ffi() {
495 let mut ctx = setup_context();
496 let ui = ctx.frame();
497
498 ui.window("list_clipper_invalid_inputs").build(|| {
499 assert!(
500 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
501 let _clipper = ListClipper::new(usize::MAX).begin(ui);
502 }))
503 .is_err()
504 );
505
506 assert!(
507 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
508 let _clipper = ListClipper::new(i32::MAX as usize).begin(ui);
509 }))
510 .is_err()
511 );
512
513 assert!(
514 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
515 let _clipper = ListClipper::new(1usize).items_height(f32::NAN).begin(ui);
516 }))
517 .is_err()
518 );
519 });
520 }
521
522 #[test]
523 fn iterator_and_display_range_use_usize_indices() {
524 let mut ctx = setup_context();
525 ctx.io_mut().set_display_size([512.0, 512.0]);
526 let ui = ctx.frame();
527
528 ui.window("list_clipper_usize_indices")
529 .size([256.0, 256.0], crate::Condition::Always)
530 .build(|| {
531 let mut clipper = ListClipper::new(3usize).items_height(1.0).begin(ui);
532 while clipper.step() {
533 for index in clipper.display_range() {
534 let _: usize = index;
535 ui.text(format!("row {index}"));
536 }
537 }
538
539 let indices: Vec<usize> = ListClipper::new(3usize)
540 .items_height(1.0)
541 .begin(ui)
542 .iter()
543 .inspect(|index| ui.text(format!("row {index}")))
544 .collect();
545 assert_eq!(indices, vec![0, 1, 2]);
546 });
547 }
548
549 #[test]
550 fn include_items_returns_non_visible_ranges_and_enforces_call_order() {
551 let mut ctx = setup_context();
552 let ui = ctx.frame();
553
554 ui.window("list_clipper_includes")
555 .size([96.0, 96.0], crate::Condition::Always)
556 .build(|| {
557 let mut clipper = ListClipper::new(1_000usize).items_height(16.0).begin(ui);
558 clipper.include_item_by_index(900);
559 clipper.include_items_by_index(950..953);
560
561 let mut included = [false; 4];
562 while clipper.step() {
563 for index in clipper.display_range() {
564 if index == 900 {
565 included[0] = true;
566 }
567 if (950..953).contains(&index) {
568 included[index - 949] = true;
569 }
570 ui.text(format!("row {index}"));
571 }
572 }
573 assert!(included.into_iter().all(|seen| seen));
574
575 assert!(
576 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
577 clipper.include_item_by_index(1);
578 }))
579 .is_err()
580 );
581 });
582 }
583
584 #[test]
585 fn include_items_rejects_invalid_ranges_before_ffi() {
586 let mut ctx = setup_context();
587 let ui = ctx.frame();
588
589 ui.window("list_clipper_invalid_includes").build(|| {
590 for (start, end) in [(5usize, 4usize), (0, 11)] {
591 let range = std::ops::Range { start, end };
592 let mut clipper = ListClipper::new(10usize).begin(ui);
593 assert!(
594 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
595 clipper.include_items_by_index(range.clone());
596 }))
597 .is_err()
598 );
599 }
600 });
601 }
602
603 #[test]
604 fn unknown_count_finish_restores_the_final_cursor_position() {
605 let mut ctx = setup_context();
606 let ui = ctx.frame();
607
608 ui.window("unknown_count_cursor").build(|| {
609 let start_y = ui.cursor_screen_pos()[1];
610 let mut clipper = ListClipper::unknown_count().items_height(10.0).begin(ui);
611 assert!(clipper.next_range().is_some());
612 clipper.finish(5);
613 let end_y = ui.cursor_screen_pos()[1];
614 assert!((end_y - start_y - 50.0).abs() < 0.01);
615 });
616 }
617
618 #[test]
619 fn unknown_count_next_range_is_fused_after_exhaustion() {
620 let mut ctx = setup_context();
621 let ui = ctx.frame();
622
623 ui.window("unknown_count_fused").build(|| {
624 let mut clipper = ListClipper::unknown_count().items_height(10.0).begin(ui);
625 let mut steps = 0;
626 while clipper.next_range().is_some() {
627 steps += 1;
628 assert!(steps < 16, "unknown-count clipper did not exhaust");
629 }
630 assert!(clipper.next_range().is_none());
631 clipper.finish(3);
632 });
633 }
634
635 #[test]
636 fn unknown_count_empty_list_needs_no_measured_height() {
637 let mut ctx = setup_context();
638 let ui = ctx.frame();
639
640 ui.window("unknown_count_empty").build(|| {
641 ListClipper::unknown_count().begin(ui).finish(0);
642 });
643 }
644
645 #[test]
646 fn unknown_count_empty_list_restores_cursor_after_a_scrolled_seek() {
647 let mut ctx = setup_context();
648 {
649 let ui = ctx.frame();
650 ui.window("unknown_count_empty_scrolled")
651 .size([96.0, 96.0], crate::Condition::Always)
652 .build(|| {
653 ui.dummy([1.0, 2_000.0]);
654 ui.set_scroll_y(500.0);
655 });
656 }
657 let _ = ctx.render_legacy();
658
659 let ui = ctx.frame();
660 ui.window("unknown_count_empty_scrolled")
661 .size([96.0, 96.0], crate::Condition::Always)
662 .build(|| {
663 assert!(ui.scroll_y() > 0.0);
664 let start_y = ui.cursor_screen_pos()[1];
665 let mut clipper = ListClipper::unknown_count().items_height(10.0).begin(ui);
666 let first_range = clipper
667 .next_range()
668 .expect("a fixed-height sentinel list should produce a visible range");
669 assert!(first_range.start > 0);
670 clipper.finish(0);
671 let end_y = ui.cursor_screen_pos()[1];
672 assert!((end_y - start_y).abs() < 0.01);
673 });
674 }
675
676 #[test]
677 fn unknown_count_finish_does_not_drain_frozen_table_rows() {
678 let mut ctx = setup_context();
679 let ui = ctx.frame();
680
681 ui.window("unknown_count_frozen_table_window").build(|| {
682 ui.table("unknown_count_frozen_table")
683 .flags(crate::TableFlags::SCROLL_Y)
684 .outer_size([96.0, 64.0])
685 .freeze(0, 1)
686 .column("value")
687 .done()
688 .build(|ui| {
689 ListClipper::unknown_count()
690 .items_height(10.0)
691 .begin(ui)
692 .finish(0);
693 });
694 });
695 }
696
697 #[test]
698 fn unknown_count_auto_height_measures_the_first_item() {
699 let mut ctx = setup_context();
700 let ui = ctx.frame();
701
702 ui.window("unknown_count_auto_height").build(|| {
703 let mut clipper = ListClipper::unknown_count().begin(ui);
704 assert_eq!(clipper.next_range(), Some(0..1));
705 ui.dummy([1.0, 10.0]);
706
707 let visible = clipper
708 .next_range()
709 .expect("measuring the first item should produce the visible range");
710 let raw = clipper.active.handle.ptr();
711 let measured_height = unsafe { (*raw).ItemsHeight };
712 assert!(measured_height.is_finite() && measured_height > 0.0);
713 for index in visible {
714 if index >= 5 {
715 break;
716 }
717 ui.dummy([1.0, 10.0]);
718 }
719 let expected_y = unsafe {
720 ((*raw).StartPosY + (*raw).StartSeekOffsetY + 5.0 * f64::from(measured_height))
721 as f32
722 };
723
724 clipper.finish(5);
725 assert!((ui.cursor_screen_pos()[1] - expected_y).abs() < 0.01);
726 });
727 }
728
729 #[test]
730 fn out_of_order_drop_defers_native_cleanup_until_lifo_is_restored() {
731 let mut ctx = setup_context();
732 let context = ctx.as_raw();
733 let ui = ctx.frame();
734
735 ui.window("list_clipper_out_of_order_drop").build(|| {
736 let outer = ListClipper::new(4).items_height(10.0).begin(ui);
737 let inner = ListClipper::unknown_count().items_height(10.0).begin(ui);
738 assert_eq!(registry::active_count(context), 2);
739
740 drop(outer);
741 assert_eq!(registry::active_count(context), 2);
742
743 drop(inner);
744 assert_eq!(registry::active_count(context), 0);
745
746 let mut next = ListClipper::new(1).items_height(10.0).begin(ui);
747 assert!(next.step());
748 ui.dummy([1.0, 10.0]);
749 assert!(!next.step());
750 });
751 }
752
753 #[test]
754 fn clipper_drop_does_not_double_panic_during_native_scope_recovery() {
755 let mut ctx = setup_context();
756 let context = ctx.as_raw();
757 let ui = ctx.frame();
758
759 ui.window("list_clipper_scope_recovery").build(|| {
760 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
761 let outer = ui.push_id("outer");
762 let _inner = ui.push_id("inner");
763 let _clipper = ListClipper::new(1).items_height(10.0).begin(ui);
764
765 drop(outer);
766 }));
767
768 assert!(result.is_err());
769 assert_eq!(registry::active_count(context), 0);
770
771 let mut next = ListClipper::new(1).items_height(10.0).begin(ui);
772 assert!(next.step());
773 ui.dummy([1.0, 10.0]);
774 assert!(!next.step());
775 });
776 }
777
778 #[test]
779 fn wrong_scope_drop_uses_layout_neutral_cleanup() {
780 let mut ctx = setup_context();
781 let context = ctx.as_raw();
782 let ui = ctx.frame();
783
784 ui.window("list_clipper_drop_scope_owner").build(|| {
785 let mut clipper = Some(ListClipper::new(1).items_height(10.0).begin(ui));
786 ui.window("list_clipper_drop_scope_other").build(|| {
787 drop(clipper.take());
788 });
789 assert_eq!(registry::active_count(context), 0);
790
791 let mut next = ListClipper::new(1).items_height(10.0).begin(ui);
792 assert!(next.step());
793 ui.dummy([1.0, 10.0]);
794 assert!(!next.step());
795 });
796 }
797
798 #[test]
799 fn clipper_dropped_after_its_window_does_not_poison_the_frame() {
800 let mut ctx = setup_context();
801 let context = ctx.as_raw();
802 let ui = ctx.frame();
803 let mut clipper = None;
804
805 ui.window("list_clipper_late_drop_owner").build(|| {
806 clipper = Some(ListClipper::new(1).items_height(10.0).begin(ui));
807 });
808 drop(clipper.take());
809 drop(clipper);
810 assert_eq!(registry::active_count(context), 0);
811 assert!(ctx.render_legacy().valid());
812 }
813
814 #[test]
815 fn forgotten_clipper_rejects_only_the_current_frame() {
816 let mut ctx = setup_context();
817 let context = ctx.as_raw();
818 let ui = ctx.frame();
819 let clipper = ui
820 .window("list_clipper_forgotten")
821 .build(|| ListClipper::new(1).items_height(10.0).begin(ui))
822 .expect("test window should be visible");
823 std::mem::forget(clipper);
824 assert_eq!(registry::active_count(context), 1);
825
826 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
827 let _ = ctx.render_legacy();
828 }));
829 assert!(result.is_err());
830 assert_eq!(registry::active_count(context), 0);
831 assert_eq!(
832 ctx.frame_lifecycle_state(),
833 crate::FrameLifecycleState::Idle
834 );
835
836 let ui = ctx.frame();
837 ui.window("list_clipper_recovered_frame").build(|| {
838 let mut next = ListClipper::new(1).items_height(10.0).begin(ui);
839 assert!(next.step());
840 ui.dummy([1.0, 10.0]);
841 assert!(!next.step());
842 });
843 assert!(ctx.render_legacy().valid());
844 }
845
846 #[test]
847 fn reopening_the_same_window_does_not_reuse_the_old_clipper_scope() {
848 let mut ctx = setup_context();
849 let context = ctx.as_raw();
850 let ui = ctx.frame();
851 let mut escaped = ui
852 .window("list_clipper_reopened_scope")
853 .build(|| ListClipper::new(1).items_height(10.0).begin(ui))
854 .expect("test window should be visible");
855 assert_eq!(registry::active_count(context), 1);
856
857 ui.window("list_clipper_reopened_scope").build(|| {
858 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
859 let _ = escaped.step();
860 }));
861 assert!(result.is_err());
862 drop(escaped);
863 assert_eq!(registry::active_count(context), 0);
864
865 drop(ListClipper::new(0).items_height(10.0).begin(ui));
866 });
867
868 assert_eq!(registry::active_count(context), 0);
869 assert!(ctx.render_legacy().valid());
870 }
871
872 #[test]
873 fn internal_window_reentry_preserves_the_active_clipper_scope() {
874 let mut ctx = setup_context();
875 let flags = ctx.io().config_flags() | crate::ConfigFlags::DOCKING_ENABLE;
876 ctx.io_mut().set_config_flags(flags);
877 let ui = ctx.frame();
878 let host_name = format!("WindowOverViewport_{:08X}", ui.main_viewport().id().raw());
879
880 ui.window(host_name).build(|| {
881 let mut clipper = ListClipper::new(1).items_height(10.0).begin(ui);
882
883 let _ = ui.dock_space_over_main_viewport_raw(0.into(), crate::DockNodeFlags::NONE);
884
885 clipper.include_item_by_index(0);
886 assert!(clipper.step());
887 ui.dummy([1.0, 10.0]);
888 assert!(!clipper.step());
889 });
890
891 assert!(ctx.render_legacy().valid());
892 }
893
894 #[test]
895 fn reopening_the_same_table_does_not_reuse_the_old_clipper_scope() {
896 let mut ctx = setup_context();
897 let context = ctx.as_raw();
898 let ui = ctx.frame();
899
900 ui.window("list_clipper_reopened_table_window").build(|| {
901 let mut escaped = {
902 let Some(_table) = ui.begin_table("list_clipper_reopened_table", 1) else {
903 panic!("test table should be visible");
904 };
905 ListClipper::new(1).items_height(10.0).begin(ui)
906 };
907 assert_eq!(registry::active_count(context), 1);
908
909 let Some(_table) = ui.begin_table("list_clipper_reopened_table", 1) else {
910 panic!("test table should be visible");
911 };
912 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
913 let _ = escaped.step();
914 }));
915 assert!(result.is_err());
916 drop(escaped);
917 assert_eq!(registry::active_count(context), 0);
918 });
919
920 assert!(ctx.render_legacy().valid());
921 }
922
923 #[test]
924 fn nested_clipper_begin_requires_the_owner_window_and_table_scope() {
925 let mut ctx = setup_context();
926 let ui = ctx.frame();
927
928 ui.window("list_clipper_begin_scope_owner").build(|| {
929 let mut outer = ListClipper::new(1).items_height(10.0).begin(ui);
930
931 ui.window("list_clipper_begin_scope_other").build(|| {
932 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
933 let _ = ListClipper::new(1).items_height(10.0).begin(ui);
934 }));
935 assert!(result.is_err());
936 });
937
938 {
939 let Some(_table) = ui.begin_table("list_clipper_begin_scope_table", 1) else {
940 panic!("test table should be visible");
941 };
942 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
943 let _ = ListClipper::new(1).items_height(10.0).begin(ui);
944 }));
945 assert!(result.is_err());
946 }
947
948 assert!(outer.step());
949 ui.dummy([1.0, 10.0]);
950 assert!(!outer.step());
951 });
952 }
953
954 #[test]
955 fn nested_clipper_operations_require_lifo_order() {
956 let mut ctx = setup_context();
957 let ui = ctx.frame();
958
959 ui.window("list_clipper_lifo_operations").build(|| {
960 let mut outer = ListClipper::new(1).items_height(10.0).begin(ui);
961 let inner = ListClipper::new(1).items_height(10.0).begin(ui);
962
963 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
964 let _ = outer.step();
965 }));
966 assert!(result.is_err());
967
968 drop(inner);
969 assert!(outer.step());
970 ui.dummy([1.0, 10.0]);
971 assert!(!outer.step());
972 });
973 }
974
975 #[test]
976 fn completed_inner_clipper_releases_the_native_lifo_scope_before_drop() {
977 let mut ctx = setup_context();
978 let context = ctx.as_raw();
979 let ui = ctx.frame();
980
981 ui.window("list_clipper_completed_inner").build(|| {
982 let mut outer = ListClipper::new(1).items_height(10.0).begin(ui);
983 let mut inner = ListClipper::new(1).items_height(10.0).begin(ui);
984
985 assert!(inner.step());
986 ui.dummy([1.0, 10.0]);
987 assert!(!inner.step());
988 assert_eq!(registry::active_count(context), 1);
989
990 assert!(outer.step());
991 ui.dummy([1.0, 10.0]);
992 assert!(!outer.step());
993 assert_eq!(registry::active_count(context), 0);
994
995 drop(inner);
996 });
997 }
998
999 #[test]
1000 fn explicitly_ended_token_does_not_block_a_different_window_scope() {
1001 let mut ctx = setup_context();
1002 let context = ctx.as_raw();
1003 let ui = ctx.frame();
1004 let ended = ui
1005 .window("list_clipper_explicit_end_owner")
1006 .build(|| {
1007 let mut clipper = ListClipper::new(1).items_height(10.0).begin(ui);
1008 clipper.end();
1009 clipper
1010 })
1011 .expect("test window should be visible");
1012 assert_eq!(registry::active_count(context), 0);
1013
1014 ui.window("list_clipper_after_explicit_end").build(|| {
1015 let mut next = ListClipper::new(1).items_height(10.0).begin(ui);
1016 assert!(next.step());
1017 ui.dummy([1.0, 10.0]);
1018 assert!(!next.step());
1019 });
1020
1021 assert!(
1022 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1023 let _ = ended.display_range();
1024 }))
1025 .is_err()
1026 );
1027 drop(ended);
1028 }
1029
1030 #[test]
1031 fn clipper_operations_require_the_begin_window_and_table_scope() {
1032 let mut ctx = setup_context();
1033 let ui = ctx.frame();
1034
1035 ui.window("list_clipper_scope_owner").build(|| {
1036 let mut clipper = ListClipper::new(1).items_height(10.0).begin(ui);
1037
1038 ui.window("list_clipper_other_window").build(|| {
1039 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1040 let _ = clipper.step();
1041 }));
1042 assert!(result.is_err());
1043 });
1044
1045 {
1046 let Some(_table) = ui.begin_table("list_clipper_other_table", 1) else {
1047 panic!("test table should be visible");
1048 };
1049 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1050 let _ = clipper.step();
1051 }));
1052 assert!(result.is_err());
1053 }
1054
1055 clipper.end();
1056 });
1057 }
1058
1059 #[test]
1060 fn unknown_count_validates_final_count_before_ffi() {
1061 assert_eq!(
1062 final_unknown_count_to_i32(
1063 i32::MAX as usize,
1064 "unknown_count_validates_final_count_before_ffi"
1065 ),
1066 i32::MAX
1067 );
1068
1069 let mut ctx = setup_context();
1070 let ui = ctx.frame();
1071 ui.window("unknown_count_overflow").build(|| {
1072 let clipper = ListClipper::unknown_count().items_height(10.0).begin(ui);
1073 assert!(
1074 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1075 clipper.finish(i32::MAX as usize + 1);
1076 }))
1077 .is_err()
1078 );
1079 });
1080 }
1081}
1082
1083pub struct ListClipperIterator<'ui> {
1084 list_clipper: ListClipperToken<'ui>,
1085 exhausted: bool,
1086 last_value: Option<usize>,
1087}
1088
1089impl<'ui> ListClipperIterator<'ui> {
1090 fn new(list_clipper: ListClipperToken<'ui>) -> Self {
1091 Self {
1092 list_clipper,
1093 exhausted: false,
1094 last_value: None,
1095 }
1096 }
1097}
1098
1099impl Iterator for ListClipperIterator<'_> {
1100 type Item = usize;
1101
1102 fn next(&mut self) -> Option<Self::Item> {
1103 loop {
1104 if let Some(value) = self.last_value {
1105 let next_value = value + 1;
1106
1107 if next_value >= self.list_clipper.display_end() {
1108 self.last_value = None;
1109 } else {
1110 self.last_value = Some(next_value);
1111 }
1112
1113 return Some(value);
1114 }
1115
1116 if self.exhausted {
1117 return None;
1119 }
1120
1121 let ret = self.list_clipper.step();
1123 if !ret {
1124 self.exhausted = true;
1125 return None;
1126 }
1127
1128 let start = self.list_clipper.display_start();
1130 let end = self.list_clipper.display_end();
1131
1132 if start < end {
1133 let next_value = start + 1;
1134 if next_value < end {
1135 self.last_value = Some(next_value);
1136 }
1137 return Some(start);
1138 } else {
1139 self.last_value = None;
1140 }
1141 }
1142 }
1143}