dear_imgui/
list_clipper.rs

1use std::marker::PhantomData;
2
3use crate::Ui;
4use crate::sys;
5
6/// Used to render only the visible items when displaying a
7/// long list of items in a scrollable area.
8///
9/// For example, you can have a huge list of checkboxes.
10/// Without the clipper you have to call `ui.checkbox(...)`
11/// for every one, even if 99% of of them are not visible in
12/// the current frame. Using the `ListClipper`, you can only
13/// call `ui.checkbox(...)` for the currently visible items.
14///
15/// Note the efficiency of list clipper relies on the height
16/// of each item being cheaply calculated. The current rust
17/// bindings only works with a fixed height for all items.
18pub struct ListClipper {
19    items_count: i32,
20    items_height: f32,
21}
22
23impl ListClipper {
24    /// Begins configuring a list clipper.
25    pub const fn new(items_count: i32) -> Self {
26        ListClipper {
27            items_count,
28            items_height: -1.0,
29        }
30    }
31
32    /// Manually set item height. If not set, the height of the first item is used for all subsequent rows.
33    pub const fn items_height(mut self, items_height: f32) -> Self {
34        self.items_height = items_height;
35        self
36    }
37
38    pub fn begin(self, ui: &Ui) -> ListClipperToken<'_> {
39        unsafe {
40            let ptr = sys::ImGuiListClipper_ImGuiListClipper();
41            sys::ImGuiListClipper_Begin(ptr, self.items_count, self.items_height);
42            ListClipperToken::new(ui, ptr)
43        }
44    }
45}
46
47/// List clipper is a mechanism to efficiently implement scrolling of
48/// large lists with random access.
49///
50/// For example you have a list of 1 million buttons, and the list
51/// clipper will help you only draw the ones which are visible.
52pub struct ListClipperToken<'ui> {
53    list_clipper: *mut sys::ImGuiListClipper,
54    _phantom: PhantomData<&'ui Ui>,
55
56    /// In upstream imgui < 1.87, calling step too many times will
57    /// cause a segfault due to null pointer. So we keep track of this
58    /// and panic instead.
59    ///
60    /// Fixed in https://github.com/ocornut/imgui/commit/dca527b which
61    /// will likely be part of imgui 1.88 - at which point this can be
62    /// removed.
63    consumed_workaround: bool,
64}
65
66impl<'ui> ListClipperToken<'ui> {
67    fn new(_: &Ui, list_clipper: *mut sys::ImGuiListClipper) -> Self {
68        Self {
69            list_clipper,
70            _phantom: PhantomData,
71            consumed_workaround: false,
72        }
73    }
74
75    /// Progress the list clipper.
76    ///
77    /// If this returns returns `true` then the you can loop between
78    /// between `clipper.display_start() .. clipper.display_end()`.
79    /// If this returns false, you must stop calling this method.
80    ///
81    /// Calling step again after it returns `false` will cause imgui
82    /// to abort. This mirrors the C++ interface.
83    ///
84    /// It is recommended to use the iterator interface!
85    pub fn step(&mut self) -> bool {
86        let is_imgui_1_88_or_higher = false;
87        if is_imgui_1_88_or_higher {
88            unsafe { sys::ImGuiListClipper_Step(self.list_clipper) }
89        } else {
90            if self.consumed_workaround {
91                panic!("ListClipperToken::step called after it has previously returned false");
92            }
93            let ret = unsafe { sys::ImGuiListClipper_Step(self.list_clipper) };
94            if !ret {
95                self.consumed_workaround = true;
96            }
97            ret
98        }
99    }
100
101    /// This is automatically called back the final call to
102    /// `step`. You can call it sooner but typically not needed.
103    pub fn end(&mut self) {
104        unsafe {
105            sys::ImGuiListClipper_End(self.list_clipper);
106        }
107    }
108
109    /// First item to call, updated each call to `step`
110    pub fn display_start(&self) -> i32 {
111        unsafe { (*self.list_clipper).DisplayStart }
112    }
113
114    /// End of items to call (exclusive), updated each call to `step`
115    pub fn display_end(&self) -> i32 {
116        unsafe { (*self.list_clipper).DisplayEnd }
117    }
118
119    /// Get an iterator which outputs all visible indexes. This is the
120    /// recommended way of using the clipper.
121    pub fn iter(self) -> ListClipperIterator<'ui> {
122        ListClipperIterator::new(self)
123    }
124}
125
126impl Drop for ListClipperToken<'_> {
127    fn drop(&mut self) {
128        unsafe {
129            sys::ImGuiListClipper_destroy(self.list_clipper);
130        };
131    }
132}
133
134pub struct ListClipperIterator<'ui> {
135    list_clipper: ListClipperToken<'ui>,
136    exhausted: bool,
137    last_value: Option<i32>,
138}
139
140impl<'ui> ListClipperIterator<'ui> {
141    fn new(list_clipper: ListClipperToken<'ui>) -> Self {
142        Self {
143            list_clipper,
144            exhausted: false,
145            last_value: None,
146        }
147    }
148}
149
150impl Iterator for ListClipperIterator<'_> {
151    type Item = i32;
152
153    fn next(&mut self) -> Option<Self::Item> {
154        if let Some(lv) = self.last_value {
155            // Currently iterating a chunk (returning all values
156            // between display_start and display_end)
157            let next_value = lv + 1;
158
159            if lv >= self.list_clipper.display_end() - 1 {
160                // If we reach the end of the current chunk, clear
161                // last_value so we call step below
162                self.last_value = None;
163            } else {
164                // Otherwise just increment it
165                self.last_value = Some(next_value);
166            }
167        }
168
169        if let Some(lv) = self.last_value {
170            // Next item within current step's chunk
171            Some(lv)
172        } else {
173            // Start iterating a new chunk
174
175            if self.exhausted {
176                // If the clipper is exhausted, don't call step again!
177                None
178            } else {
179                // Advance the clipper
180                let ret = self.list_clipper.step();
181                if !ret {
182                    self.exhausted = true;
183                    None
184                } else {
185                    // Setup iteration for this step's chunk
186                    let start = self.list_clipper.display_start();
187                    let end = self.list_clipper.display_end();
188
189                    if start == end {
190                        // Somewhat special case: if a single item, we
191                        // don't store the last_value so we call
192                        // step() again next iteration
193                        self.last_value = None;
194                    } else {
195                        self.last_value = Some(start);
196                    }
197                    Some(start)
198                }
199            }
200        }
201    }
202}