1use super::*;
2use crate::bindings::ui;
3use crate::controls::selection_area;
4use crate::controls::SelectionArea;
5use crate::frame_scheduler::mark_needs_commit;
6use crate::signal::Subscription;
7use std::any::Any;
8use std::cell::{Cell, RefCell};
9use std::rc::Rc;
10
11const FULL_SIZE: f32 = 100.0;
12const DEFAULT_MAX_VISIBLE_ITEMS: i32 = 20;
13const POOL_OVERSCAN_ITEMS: i32 = 2;
14const MISSING_BIND_ITEM_MESSAGE: &str =
15 "VirtualList: item renderer not configured. Call .on_bind_item() after construction.";
16
17type VirtualListBinder = Rc<dyn Fn(usize, i32)>;
18
19struct VirtualListInner {
20 root: FlexBox,
21 total_items: Cell<i32>,
22 item_height: f32,
23 bind_item: RefCell<VirtualListBinder>,
24 scroll_state: ScrollState,
25 scroll_box: ScrollBox,
26 top_spacer: FlexBox,
27 bottom_spacer: FlexBox,
28 pool_size: i32,
29 pool_rows: Vec<SelectionArea>,
30 pool_containers: Vec<FlexBox>,
31 row_state_owner: RefCell<Option<Rc<dyn Any>>>,
32 pool_item_index_by_row: RefCell<Vec<i32>>,
33 subscriptions: RefCell<Vec<Subscription>>,
34 current_first_visible_index: Cell<i32>,
35 current_last_visible_index: Cell<i32>,
36}
37
38pub struct VirtualList<T = FlexBox> {
39 inner: Rc<VirtualListInner>,
40 row_states: Rc<Vec<T>>,
41}
42
43impl<T> Clone for VirtualList<T> {
44 fn clone(&self) -> Self {
45 Self {
46 inner: self.inner.clone(),
47 row_states: self.row_states.clone(),
48 }
49 }
50}
51
52impl VirtualList<FlexBox> {
53 pub fn new(total_items: i32, item_height: f32) -> Self {
54 Self::with_max_visible(total_items, item_height, DEFAULT_MAX_VISIBLE_ITEMS)
55 }
56
57 pub fn with_max_visible(total_items: i32, item_height: f32, max_visible: i32) -> Self {
58 let total_items = total_items.max(0);
59 let item_height = if item_height > 0.0 { item_height } else { 1.0 };
60 let pool_size = if max_visible > 0 {
61 max_visible + POOL_OVERSCAN_ITEMS
62 } else {
63 POOL_OVERSCAN_ITEMS
64 };
65 let scroll_state = ScrollState::new();
66 let top_spacer = flex_box();
67 top_spacer
68 .width(FULL_SIZE, Unit::Percent)
69 .height(0.0, Unit::Pixel);
70 let bottom_spacer = flex_box();
71 bottom_spacer
72 .width(FULL_SIZE, Unit::Percent)
73 .height(0.0, Unit::Pixel);
74 let content = column();
75 content.width(FULL_SIZE, Unit::Percent).child(&top_spacer);
76
77 let mut pool_rows = Vec::new();
78 let mut pool_containers = Vec::new();
79 let mut pool_item_index_by_row = Vec::new();
80 for _ in 0..pool_size {
81 let container = flex_box();
82 container.fill_size();
83 let row_area = selection_area();
84 row_area
85 .width(FULL_SIZE, Unit::Percent)
86 .height(0.0, Unit::Pixel)
87 .child(&container);
88 content.child(&row_area);
89 pool_rows.push(row_area);
90 pool_containers.push(container);
91 pool_item_index_by_row.push(-1);
92 }
93 content.child(&bottom_spacer);
94
95 let scroll_box = ScrollBox::with_parts(scroll_state.clone(), ScrollView::new());
96 scroll_box
97 .scroll_enabled_x(false)
98 .scroll_enabled_y(true)
99 .scroll_offset(scroll_state.offset_x(), scroll_state.offset_y())
100 .scroll_content_size(-1.0, total_items as f32 * item_height)
101 .fill_size()
102 .child(&content);
103 scroll_state.set_content_height(total_items as f32 * item_height);
104
105 let root = column();
106 root.fill_size().child(&scroll_box);
107
108 let inner = Rc::new(VirtualListInner {
109 root,
110 total_items: Cell::new(total_items),
111 item_height,
112 bind_item: RefCell::new(Rc::new(|_, _| panic!("{MISSING_BIND_ITEM_MESSAGE}"))),
113 scroll_state,
114 scroll_box,
115 top_spacer,
116 bottom_spacer,
117 pool_size,
118 pool_rows,
119 pool_containers: pool_containers.clone(),
120 row_state_owner: RefCell::new(None),
121 pool_item_index_by_row: RefCell::new(pool_item_index_by_row),
122 subscriptions: RefCell::new(Vec::new()),
123 current_first_visible_index: Cell::new(-1),
124 current_last_visible_index: Cell::new(-1),
125 });
126 let row_states = Rc::new(pool_containers);
127 inner
128 .row_state_owner
129 .replace(Some(row_states.clone() as Rc<dyn Any>));
130 let list = Self { inner, row_states };
131 list.attach_listeners();
132 list
133 }
134}
135
136impl<T: 'static> VirtualList<T> {
137 pub fn item_template<U: 'static>(self, template: impl Fn(&FlexBox) -> U) -> VirtualList<U> {
138 let row_states = Rc::new(
139 self.inner
140 .pool_containers
141 .iter()
142 .map(template)
143 .collect::<Vec<_>>(),
144 );
145 self.inner
146 .row_state_owner
147 .replace(Some(row_states.clone() as Rc<dyn Any>));
148 *self.inner.bind_item.borrow_mut() = Rc::new(|_, _| panic!("{MISSING_BIND_ITEM_MESSAGE}"));
149 self.inner.current_first_visible_index.set(-1);
150 self.inner.current_last_visible_index.set(-1);
151 self.inner.subscriptions.borrow_mut().clear();
152 let list = VirtualList {
153 inner: self.inner,
154 row_states,
155 };
156 list.attach_listeners();
157 list
158 }
159
160 pub fn scroll_state(&self) -> ScrollState {
161 self.inner.scroll_state.clone()
162 }
163
164 pub fn total_items(&self) -> i32 {
165 self.inner.total_items.get()
166 }
167
168 pub fn scroll_box(&self) -> ScrollBox {
169 self.inner.scroll_box.clone()
170 }
171
172 pub fn item_height(&self) -> f32 {
173 self.inner.item_height
174 }
175
176 pub fn total_content_height(&self) -> f32 {
177 self.total_items() as f32 * self.item_height()
178 }
179
180 pub fn first_visible_index(&self) -> i32 {
181 let current = self.inner.current_first_visible_index.get();
182 if current >= 0 {
183 current
184 } else {
185 0
186 }
187 }
188
189 pub fn rendered_item_count(&self) -> i32 {
190 let first = self.inner.current_first_visible_index.get();
191 let last = self.inner.current_last_visible_index.get();
192 if first < 0 || last < first {
193 0
194 } else {
195 last - first
196 }
197 }
198
199 pub fn is_selection_barrier(&self) -> bool {
200 true
201 }
202
203 pub fn render(&self) -> &Self {
204 self.rebuild_visible_range(true);
205 self
206 }
207
208 pub fn node_id(&self, id: impl Into<String>) -> &Self {
209 self.inner.scroll_box.node_id(id);
210 self
211 }
212
213 pub fn persist_scroll(&self, flag: bool) -> &Self {
214 self.inner.scroll_box.persist_scroll(flag);
215 self
216 }
217
218 pub fn width(&self, value: f32, unit: Unit) -> &Self {
219 self.inner.root.width(value, unit);
220 if unit == Unit::Pixel {
221 self.inner.scroll_state.set_viewport_width(value);
222 }
223 self
224 }
225
226 pub fn height(&self, value: f32, unit: Unit) -> &Self {
227 self.inner.root.height(value, unit);
228 if unit == Unit::Pixel {
229 self.inner.scroll_state.set_viewport_height(value);
230 }
231 self
232 }
233
234 pub fn on_bind_item(&self, renderer: impl Fn(&T, i32) + 'static) -> &Self {
235 let row_states = self.row_states.clone();
236 *self.inner.bind_item.borrow_mut() = Rc::new(move |pool_index, item_index| {
237 renderer(&row_states[pool_index], item_index);
238 });
239 self.inner.current_first_visible_index.set(-1);
240 self.inner.current_last_visible_index.set(-1);
241 self.rebuild_visible_range(true);
242 self
243 }
244
245 pub fn update_item_count(&self, next: i32) {
246 self.inner.total_items.set(next.max(0));
247 self.inner
248 .scroll_state
249 .set_content_height(self.total_content_height());
250 self.inner
251 .scroll_box
252 .scroll_content_size(-1.0, self.total_content_height());
253 self.inner.current_first_visible_index.set(-1);
254 self.inner.current_last_visible_index.set(-1);
255
256 let clamped_offset = self.max_offset_for_current_viewport();
257 if self.inner.scroll_state.offset_y() > clamped_offset {
258 self.inner
259 .scroll_box
260 .scroll_offset(self.inner.scroll_state.offset_x(), clamped_offset);
261 return;
262 }
263 self.rebuild_visible_range(true);
264 }
265
266 fn attach_listeners(&self) {
267 let weak = Rc::downgrade(&self.inner);
268 let weak_row_states = Rc::downgrade(&self.row_states);
269 self.inner
270 .subscriptions
271 .borrow_mut()
272 .push(self.inner.scroll_state.subscribe_offset_y(move || {
273 if let (Some(inner), Some(row_states)) = (weak.upgrade(), weak_row_states.upgrade())
274 {
275 VirtualList { inner, row_states }.handle_scroll_offset_changed();
276 }
277 }));
278 let weak = Rc::downgrade(&self.inner);
279 let weak_row_states = Rc::downgrade(&self.row_states);
280 self.inner.subscriptions.borrow_mut().push(
281 self.inner.scroll_state.subscribe_viewport_height(move || {
282 if let (Some(inner), Some(row_states)) = (weak.upgrade(), weak_row_states.upgrade())
283 {
284 VirtualList { inner, row_states }.handle_metrics_changed();
285 }
286 }),
287 );
288 let weak = Rc::downgrade(&self.inner);
289 let weak_row_states = Rc::downgrade(&self.row_states);
290 self.inner.subscriptions.borrow_mut().push(
291 self.inner.scroll_state.subscribe_content_height(move || {
292 if let (Some(inner), Some(row_states)) = (weak.upgrade(), weak_row_states.upgrade())
293 {
294 VirtualList { inner, row_states }.handle_metrics_changed();
295 }
296 }),
297 );
298 }
299
300 fn handle_metrics_changed(&self) {
301 self.rebuild_visible_range(true);
302 }
303
304 fn handle_scroll_offset_changed(&self) {
305 self.rebuild_visible_range(true);
306 }
307
308 fn rebuild_visible_range(&self, commit: bool) {
309 let total_items = self.total_items();
310 if total_items <= 0 {
311 if self.inner.current_first_visible_index.get() == 0
312 && self.inner.current_last_visible_index.get() == 0
313 {
314 return;
315 }
316 self.inner.current_first_visible_index.set(0);
317 self.inner.current_last_visible_index.set(0);
318 self.inner.top_spacer.height(0.0, Unit::Pixel);
319 self.inner.bottom_spacer.height(0.0, Unit::Pixel);
320 for pool_index in 0..self.inner.pool_size as usize {
321 self.hide_pool_item(pool_index);
322 }
323 self.commit_if_built(commit);
324 return;
325 }
326
327 let mut first_visible_index =
328 (self.inner.scroll_state.offset_y() / self.inner.item_height).floor() as i32;
329 if first_visible_index < 0 {
330 first_visible_index = 0;
331 }
332 if first_visible_index > total_items {
333 first_visible_index = total_items;
334 }
335
336 let viewport_height = if self.inner.scroll_state.viewport_height() > 0.0 {
337 self.inner.scroll_state.viewport_height()
338 } else {
339 self.inner.item_height
340 };
341 let mut visible_count = (viewport_height / self.inner.item_height).ceil() as i32 + 1;
342 if visible_count < 1 {
343 visible_count = 1;
344 }
345 if visible_count > self.inner.pool_size {
346 visible_count = self.inner.pool_size;
347 }
348
349 let mut last_visible_index = first_visible_index + visible_count;
350 if last_visible_index > total_items {
351 last_visible_index = total_items;
352 }
353
354 if first_visible_index == self.inner.current_first_visible_index.get()
355 && last_visible_index == self.inner.current_last_visible_index.get()
356 {
357 return;
358 }
359
360 self.inner
361 .current_first_visible_index
362 .set(first_visible_index);
363 self.inner
364 .current_last_visible_index
365 .set(last_visible_index);
366
367 let top_spacer_height = first_visible_index as f32 * self.inner.item_height;
368 let mut bottom_spacer_height =
369 self.total_content_height() - (last_visible_index as f32 * self.inner.item_height);
370 if bottom_spacer_height < 0.0 {
371 bottom_spacer_height = 0.0;
372 }
373
374 self.inner.top_spacer.height(top_spacer_height, Unit::Pixel);
375 self.inner
376 .bottom_spacer
377 .height(bottom_spacer_height, Unit::Pixel);
378
379 let previous_item_index_by_row = self.inner.pool_item_index_by_row.borrow().clone();
380 let visible_items = (last_visible_index - first_visible_index) as usize;
381
382 for (pool_index, previous_item_index) in previous_item_index_by_row
383 .iter()
384 .copied()
385 .enumerate()
386 .take(self.inner.pool_size as usize)
387 {
388 if previous_item_index != -1
389 && (previous_item_index < first_visible_index
390 || previous_item_index >= last_visible_index)
391 {
392 self.clear_row_selection(pool_index);
393 }
394 }
395
396 for pool_index in 0..self.inner.pool_size as usize {
397 let row_area = &self.inner.pool_rows[pool_index];
398 if pool_index < visible_items {
399 let next_item_index = first_visible_index + pool_index as i32;
400 row_area.height(self.inner.item_height, Unit::Pixel);
401 self.render_item(pool_index, next_item_index);
402 self.inner.pool_item_index_by_row.borrow_mut()[pool_index] = next_item_index;
403 } else {
404 self.hide_pool_item(pool_index);
405 }
406 }
407
408 for pool_index in 0..visible_items {
409 let next_item_index = first_visible_index + pool_index as i32;
410 let previous_pool_index =
411 Self::find_pool_index_for_item(&previous_item_index_by_row, next_item_index);
412 if previous_pool_index != -1 && previous_pool_index as usize != pool_index {
413 self.retarget_row_selection(previous_pool_index as usize, pool_index);
414 }
415 }
416
417 self.commit_if_built(commit);
418 }
419
420 fn hide_pool_item(&self, pool_index: usize) {
421 if self.inner.pool_item_index_by_row.borrow()[pool_index] != -1 {
422 self.clear_row_selection(pool_index);
423 self.inner.pool_item_index_by_row.borrow_mut()[pool_index] = -1;
424 }
425 self.inner.pool_rows[pool_index].height(0.0, Unit::Pixel);
426 self.clear_item_node(&self.inner.pool_containers[pool_index].retained_node_ref());
427 }
428
429 fn clear_row_selection(&self, pool_index: usize) {
430 if !self.has_built_handle() {
431 return;
432 }
433 self.clear_selection_node(&self.inner.pool_rows[pool_index].retained_node_ref());
434 }
435
436 fn retarget_row_selection(&self, from_pool_index: usize, to_pool_index: usize) {
437 if !self.has_built_handle() {
438 return;
439 }
440 let mut from_texts = Vec::new();
441 let mut to_texts = Vec::new();
442 self.collect_text_nodes(
443 &self.inner.pool_rows[from_pool_index].retained_node_ref(),
444 &mut from_texts,
445 );
446 self.collect_text_nodes(
447 &self.inner.pool_rows[to_pool_index].retained_node_ref(),
448 &mut to_texts,
449 );
450 let pair_count = from_texts.len().min(to_texts.len());
451 for index in 0..pair_count {
452 let from_text = from_texts[index];
453 let to_text = to_texts[index];
454 if from_text != NodeHandle::INVALID
455 && to_text != NodeHandle::INVALID
456 && from_text != to_text
457 {
458 ui::retarget_selection(from_text.raw(), to_text.raw());
459 }
460 }
461 }
462
463 fn collect_text_nodes(&self, node: &NodeRef, out: &mut Vec<NodeHandle>) {
464 if node.text_content_for_routing().is_some() {
465 out.push(node.handle());
466 return;
467 }
468 for child in node.children() {
469 self.collect_text_nodes(&child, out);
470 }
471 }
472
473 fn clear_selection_node(&self, node: &NodeRef) {
474 if node.text_content_for_routing().is_some() {
475 let handle = node.handle();
476 if handle != NodeHandle::INVALID {
477 ui::clear_selection(handle.raw());
478 }
479 return;
480 }
481 for child in node.children() {
482 self.clear_selection_node(&child);
483 }
484 }
485
486 fn clear_item_node(&self, node: &NodeRef) {
487 node.set_semantic_label_for_routing(Some(String::new()));
488 let handle = node.handle();
489 if handle != NodeHandle::INVALID {
490 ui::set_semantic_label(handle.raw(), "");
491 }
492 if node.text_content_for_routing().is_some() {
493 node.set_text_content_for_routing(Some(String::new()));
494 if handle != NodeHandle::INVALID {
495 ui::set_text(handle.raw(), "");
496 }
497 }
498 for child in node.children() {
499 self.clear_item_node(&child);
500 }
501 }
502
503 fn commit_if_built(&self, commit: bool) {
504 if !commit || !self.has_built_handle() {
505 return;
506 }
507 mark_needs_commit();
508 }
509
510 fn render_item(&self, pool_index: usize, index: i32) {
511 let binder = self.inner.bind_item.borrow().clone();
512 binder(pool_index, index);
513 }
514
515 fn max_offset_for_current_viewport(&self) -> f32 {
516 let viewport_height = if self.inner.scroll_state.viewport_height() > 0.0 {
517 self.inner.scroll_state.viewport_height()
518 } else {
519 0.0
520 };
521 let max_offset = self.total_content_height() - viewport_height;
522 if max_offset > 0.0 {
523 max_offset
524 } else {
525 0.0
526 }
527 }
528
529 fn find_pool_index_for_item(previous_item_index_by_row: &[i32], item_index: i32) -> i32 {
530 previous_item_index_by_row
531 .iter()
532 .position(|candidate| *candidate == item_index)
533 .map(|index| index as i32)
534 .unwrap_or(-1)
535 }
536}
537
538impl<T: 'static> Node for VirtualList<T> {
539 fn retained_node_ref(&self) -> NodeRef {
540 let list = self.clone();
541 self.inner
542 .root
543 .retained_node_ref()
544 .with_build_callback(move || list.build())
545 }
546
547 fn build(&self) {
548 self.ensure_handle();
549 self.inner.root.build_self();
550 self.build_children();
551 if self.inner.current_first_visible_index.get() < 0 && self.total_items() > 0 {
552 self.rebuild_visible_range(false);
553 }
554 ui::set_selection_area_barrier(self.handle().raw(), true);
555 }
556
557 fn build_self(&self) {
558 self.inner.root.build_self();
559 }
560
561 fn dispose(&self) {
562 self.inner.subscriptions.borrow_mut().clear();
563 self.inner.root.dispose();
564 }
565}
566
567impl<T: 'static> HasFlexBoxRoot for VirtualList<T> {
568 fn flex_box_root(&self) -> &FlexBox {
569 &self.inner.root
570 }
571
572 fn set_flex_box_surface_width(&self, value: f32, unit: Unit) {
573 self.width(value, unit);
574 }
575
576 fn set_flex_box_surface_height(&self, value: f32, unit: Unit) {
577 self.height(value, unit);
578 }
579}
580
581impl<T: 'static> ThemeBindable for VirtualList<T> {
582 fn theme_binding_node(&self) -> NodeRef {
583 self.inner.root.retained_node_ref()
584 }
585
586 fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
587 let weak = Rc::downgrade(&self.inner);
588 let weak_row_states = Rc::downgrade(&self.row_states);
589 Box::new(move || {
590 Some(VirtualList {
591 inner: weak.upgrade()?,
592 row_states: weak_row_states.upgrade()?,
593 })
594 })
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use crate::ffi::{self, Call};
602 use crate::prelude::*;
603 use std::collections::HashMap;
604
605 fn tracked_bind_virtual_list_item(
606 rendered_indices: &Rc<RefCell<Vec<i32>>>,
607 labels: &Rc<RefCell<HashMap<usize, TextNode>>>,
608 container: &FlexBox,
609 index: i32,
610 ) {
611 rendered_indices.borrow_mut().push(index);
612 let key = std::ptr::from_ref(container) as usize;
613 let existing = { labels.borrow().get(&key).cloned() };
614 let label = if let Some(existing) = existing {
615 existing
616 } else {
617 let label = text("");
618 container.child(&label);
619 labels.borrow_mut().insert(key, label.clone());
620 label
621 };
622 let text_value = format!("Item {}", index);
623 label.text(&text_value);
624 label.semantic_label(&text_value);
625 }
626
627 fn static_bind_virtual_list_item(
628 labels: &Rc<RefCell<HashMap<usize, TextNode>>>,
629 container: &FlexBox,
630 index: i32,
631 ) {
632 let key = std::ptr::from_ref(container) as usize;
633 let existing = { labels.borrow().get(&key).cloned() };
634 let label = if let Some(existing) = existing {
635 existing
636 } else {
637 let label = text("");
638 container.child(&label);
639 labels.borrow_mut().insert(key, label.clone());
640 label
641 };
642 label.text(format!("row {}", index));
643 }
644
645 #[test]
646 fn binds_only_the_visible_window_for_a_fixed_height_list() {
647 ffi::test::reset();
648 let rendered_indices = Rc::new(RefCell::new(Vec::new()));
649 let labels = Rc::new(RefCell::new(HashMap::new()));
650 let list = VirtualList::new(10_000, 20.0);
651 let captured_indices = rendered_indices.clone();
652 let captured_labels = labels.clone();
653 list.on_bind_item(move |container, index| {
654 tracked_bind_virtual_list_item(&captured_indices, &captured_labels, container, index);
655 });
656 list.width(180.0, Unit::Pixel);
657 list.height(100.0, Unit::Pixel);
658
659 list.build();
660
661 let rendered = rendered_indices.borrow();
662 let last_window_start = rendered.len() - 6;
663 assert_eq!(rendered[last_window_start], 0);
664 assert_eq!(rendered[last_window_start + 5], 5);
665 assert_eq!(list.first_visible_index(), 0);
666 assert_eq!(list.rendered_item_count(), 6);
667 list.dispose();
668 }
669
670 #[test]
671 fn rebinds_pooled_rows_without_recreating_nodes_when_scroll_offset_changes() {
672 ffi::test::reset();
673 let rendered_indices = Rc::new(RefCell::new(Vec::new()));
674 let labels = Rc::new(RefCell::new(HashMap::new()));
675 let list = VirtualList::new(10_000, 20.0);
676 let captured_indices = rendered_indices.clone();
677 let captured_labels = labels.clone();
678 list.on_bind_item(move |container, index| {
679 tracked_bind_virtual_list_item(&captured_indices, &captured_labels, container, index);
680 });
681 list.width(180.0, Unit::Pixel);
682 list.height(100.0, Unit::Pixel);
683 Application::mount(list.clone());
684
685 rendered_indices.borrow_mut().clear();
686 ffi::test::reset();
687 list.scroll_state().set_offset_y(60.0);
688
689 let rendered = rendered_indices.borrow();
690 assert_eq!(rendered.len(), 6);
691 assert_eq!(rendered[0], 3);
692 assert_eq!(rendered[5], 8);
693 assert_eq!(list.first_visible_index(), 3);
694 let calls = ffi::test::take_calls();
695 assert!(!calls
696 .iter()
697 .any(|call| matches!(call, Call::CreateNode { .. })));
698 assert!(!calls
699 .iter()
700 .any(|call| matches!(call, Call::DeleteNode { .. })));
701 assert!(calls
702 .iter()
703 .any(|call| matches!(call, Call::SetText { text, .. } if text == "Item 3")));
704 assert!(calls.iter().any(|call| {
705 matches!(
706 call,
707 Call::SetHeight {
708 value,
709 unit_enum,
710 ..
711 } if *value == 20.0 && *unit_enum == Unit::Pixel as u32
712 )
713 }));
714 Application::unmount();
715 }
716
717 #[test]
718 fn tracks_content_height_and_clamps_the_visible_window_when_item_count_changes() {
719 ffi::test::reset();
720 let labels = Rc::new(RefCell::new(HashMap::new()));
721 let list = VirtualList::new(10_000, 24.0);
722 let captured_labels = labels.clone();
723 list.on_bind_item(move |container, index| {
724 static_bind_virtual_list_item(&captured_labels, container, index);
725 });
726 list.height(120.0, Unit::Pixel);
727
728 assert_eq!(list.scroll_state().content_height(), 240_000.0);
729 assert_eq!(list.rendered_item_count(), 6);
730
731 list.update_item_count(5);
732
733 assert_eq!(list.scroll_state().content_height(), 120.0);
734 assert_eq!(list.rendered_item_count(), 5);
735 list.dispose();
736 }
737
738 #[test]
739 fn supports_owner_captured_renderers_with_on_bind_item() {
740 ffi::test::reset();
741 let rendered_indices = Rc::new(RefCell::new(Vec::new()));
742 let labels = Rc::new(RefCell::new(HashMap::new()));
743 let list = VirtualList::new(10_000, 20.0);
744 let captured_indices = rendered_indices.clone();
745 let captured_labels = labels.clone();
746 list.on_bind_item(move |container, index| {
747 tracked_bind_virtual_list_item(&captured_indices, &captured_labels, container, index);
748 });
749 list.width(180.0, Unit::Pixel);
750 list.height(100.0, Unit::Pixel);
751
752 list.build();
753
754 let rendered = rendered_indices.borrow();
755 assert!(rendered.len() >= 6);
756 assert_eq!(rendered[0], 0);
757 assert_eq!(rendered[rendered.len() - 1], 5);
758 list.dispose();
759 }
760
761 #[test]
762 fn typed_item_template_creates_one_retained_state_per_pool_row_and_rebinds_it() {
763 #[derive(Clone)]
764 struct Row {
765 label: TextNode,
766 }
767
768 ffi::test::reset();
769 let template_count = Rc::new(Cell::new(0));
770 let captured_template_count = template_count.clone();
771 let list = VirtualList::with_max_visible(10_000, 20.0, 4).item_template(move |container| {
772 captured_template_count.set(captured_template_count.get() + 1);
773 let label = text("");
774 container.child(&label);
775 Row { label }
776 });
777 let bound_indices = Rc::new(RefCell::new(Vec::new()));
778 let captured_indices = bound_indices.clone();
779 list.on_bind_item(move |row, index| {
780 captured_indices.borrow_mut().push(index);
781 row.label.text(format!("typed item {index}"));
782 });
783 list.height(60.0, Unit::Pixel);
784 Application::mount(list.clone());
785
786 assert_eq!(template_count.get(), 6);
787 bound_indices.borrow_mut().clear();
788 ffi::test::reset();
789 list.scroll_state().set_offset_y(40.0);
790
791 assert_eq!(&*bound_indices.borrow(), &[2, 3, 4, 5]);
792 assert!(!ffi::test::take_calls()
793 .iter()
794 .any(|call| matches!(call, Call::CreateNode { .. } | Call::DeleteNode { .. })));
795 Application::unmount();
796 }
797}