1use std::{
14 cell::RefCell,
15 cmp,
16 ops::{Deref, Range},
17 rc::Rc,
18};
19
20use gpui::{
21 Along, AnyElement, App, AvailableSpace, Axis, Bounds, ContentMask, Context,
22 DeferredScrollToItem, Div, Element, ElementId, Entity, GlobalElementId, Half, Hitbox,
23 InteractiveElement, IntoElement, IsZero as _, ListSizingBehavior, Pixels, Point, Render,
24 ScrollHandle, ScrollStrategy, Size, Stateful, StatefulInteractiveElement, StyleRefinement,
25 Styled, Window, div, point, px, size,
26};
27use smallvec::SmallVec;
28
29use crate::{AxisExt, InteractiveElementExt as _};
30
31struct VirtualListScrollHandleState {
32 axis: Axis,
33 items_count: usize,
34 last_content_size: Option<Size<Pixels>>,
36 pub deferred_scroll_to_item: Option<DeferredScrollToItem>,
37}
38
39#[derive(Clone)]
43pub struct VirtualListScrollHandle {
44 state: Rc<RefCell<VirtualListScrollHandleState>>,
45 base_handle: ScrollHandle,
46}
47
48impl From<ScrollHandle> for VirtualListScrollHandle {
49 fn from(handle: ScrollHandle) -> Self {
50 let mut this = VirtualListScrollHandle::new();
51 this.base_handle = handle;
52 this
53 }
54}
55
56impl AsRef<ScrollHandle> for VirtualListScrollHandle {
57 fn as_ref(&self) -> &ScrollHandle {
58 &self.base_handle
59 }
60}
61
62impl crate::ScrollbarHandle for VirtualListScrollHandle {
63 fn viewport_bounds(&self) -> Bounds<Pixels> {
64 self.base_handle.bounds()
65 }
66
67 fn offset(&self) -> Point<Pixels> {
68 self.base_handle.offset()
69 }
70
71 fn set_offset(&self, offset: Point<Pixels>) {
72 self.base_handle.set_offset(offset);
73 }
74
75 fn content_size(&self) -> Size<Pixels> {
76 self.base_handle.content_size()
77 }
78}
79
80impl Deref for VirtualListScrollHandle {
81 type Target = ScrollHandle;
82
83 fn deref(&self) -> &Self::Target {
84 &self.base_handle
85 }
86}
87
88impl VirtualListScrollHandle {
89 pub fn new() -> Self {
91 VirtualListScrollHandle {
92 state: Rc::new(RefCell::new(VirtualListScrollHandleState {
93 axis: Axis::Vertical,
94 items_count: 0,
95 last_content_size: None,
96 deferred_scroll_to_item: None,
97 })),
98 base_handle: ScrollHandle::default(),
99 }
100 }
101
102 pub fn base_handle(&self) -> &ScrollHandle {
104 &self.base_handle
105 }
106
107 pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
109 self.scroll_to_item_with_offset(ix, strategy, 0);
110 }
111
112 fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) {
114 let mut state = self.state.borrow_mut();
115 state.deferred_scroll_to_item = Some(DeferredScrollToItem {
116 item_index: ix,
117 strategy,
118 offset,
119 scroll_strict: false,
120 });
121 }
122
123 pub fn scroll_to_bottom(&self) {
125 let items_count = self.state.borrow().items_count;
126 self.scroll_to_item(items_count.saturating_sub(1), ScrollStrategy::Top);
127 }
128}
129
130#[inline]
140pub fn v_virtual_list<R, V>(
141 view: Entity<V>,
142 id: impl Into<ElementId>,
143 item_sizes: Rc<Vec<Size<Pixels>>>,
144 f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
145) -> VirtualList
146where
147 R: IntoElement,
148 V: Render,
149{
150 virtual_list(view, id, Axis::Vertical, item_sizes, f)
151}
152
153#[inline]
161pub fn h_virtual_list<R, V>(
162 view: Entity<V>,
163 id: impl Into<ElementId>,
164 item_sizes: Rc<Vec<Size<Pixels>>>,
165 f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
166) -> VirtualList
167where
168 R: IntoElement,
169 V: Render,
170{
171 virtual_list(view, id, Axis::Horizontal, item_sizes, f)
172}
173
174#[doc(hidden)]
175pub fn virtual_list<R, V>(
176 view: Entity<V>,
177 id: impl Into<ElementId>,
178 axis: Axis,
179 item_sizes: Rc<Vec<Size<Pixels>>>,
180 f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
181) -> VirtualList
182where
183 R: IntoElement,
184 V: Render,
185{
186 let id: ElementId = id.into();
187 let scroll_handle = VirtualListScrollHandle::new();
188 let render_range = move |visible_range, window: &mut Window, cx: &mut App| {
189 view.update(cx, |this, cx| {
190 f(this, visible_range, window, cx)
191 .into_iter()
192 .map(|component| component.into_any_element())
193 .collect()
194 })
195 };
196
197 VirtualList {
198 id: id.clone(),
199 axis,
200 base: div()
201 .id(id)
202 .size_full()
203 .overflow_scroll()
204 .lock_scroll_axis()
205 .track_scroll(&scroll_handle),
206 scroll_handle,
207 items_count: item_sizes.len(),
208 item_sizes,
209 render_items: Box::new(render_range),
210 sizing_behavior: ListSizingBehavior::default(),
211 item_to_measure_index: 0,
212 }
213}
214
215pub struct VirtualList {
217 id: ElementId,
218 axis: Axis,
219 base: Stateful<Div>,
220 scroll_handle: VirtualListScrollHandle,
221 items_count: usize,
222 item_sizes: Rc<Vec<Size<Pixels>>>,
223 render_items: Box<
224 dyn for<'a> Fn(Range<usize>, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>,
225 >,
226 sizing_behavior: ListSizingBehavior,
227 item_to_measure_index: usize,
228}
229
230impl Styled for VirtualList {
231 fn style(&mut self) -> &mut StyleRefinement {
232 self.base.style()
233 }
234}
235
236impl VirtualList {
237 pub fn track_scroll(mut self, scroll_handle: &VirtualListScrollHandle) -> Self {
238 self.base = self.base.track_scroll(&scroll_handle);
239 self.scroll_handle = scroll_handle.clone();
240 self
241 }
242
243 pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
245 self.sizing_behavior = behavior;
246 self
247 }
248
249 pub fn with_item_to_measure_index(mut self, index: usize) -> Self {
251 self.item_to_measure_index = index;
252 self
253 }
254
255 #[doc(hidden)]
259 pub fn with_scroll_handle(mut self, scroll_handle: &VirtualListScrollHandle) -> Self {
260 self.base = div().id(self.id.clone()).size_full();
261 self.scroll_handle = scroll_handle.clone();
262 self
263 }
264
265 fn scroll_to_deferred_item(
266 &self,
267 scroll_offset: Point<Pixels>,
268 items_bounds: &[Bounds<Pixels>],
269 content_bounds: &Bounds<Pixels>,
270 scroll_to_item: DeferredScrollToItem,
271 ) -> Point<Pixels> {
272 let Some(bounds) = items_bounds
273 .get(scroll_to_item.item_index + scroll_to_item.offset)
274 .cloned()
275 else {
276 return scroll_offset;
277 };
278
279 let mut scroll_offset = scroll_offset;
280 match scroll_to_item.strategy {
281 ScrollStrategy::Center => {
282 if self.axis.is_vertical() {
283 scroll_offset.y = content_bounds.top() + content_bounds.size.height.half()
284 - bounds.top()
285 - bounds.size.height.half()
286 } else {
287 scroll_offset.x = content_bounds.left() + content_bounds.size.width.half()
288 - bounds.left()
289 - bounds.size.width.half()
290 }
291 }
292 _ => {
293 if self.axis.is_vertical() {
295 if bounds.top() + scroll_offset.y < content_bounds.top() {
296 scroll_offset.y = content_bounds.top() - bounds.top()
297 } else if bounds.bottom() + scroll_offset.y > content_bounds.bottom() {
298 scroll_offset.y = content_bounds.bottom() - bounds.bottom();
299 }
300 } else {
301 if bounds.left() + scroll_offset.x < content_bounds.left() {
302 scroll_offset.x = content_bounds.left() - bounds.left();
303 } else if bounds.right() + scroll_offset.x > content_bounds.right() {
304 scroll_offset.x = content_bounds.right() - bounds.right();
305 }
306 }
307 }
308 }
309 self.scroll_handle.set_offset(scroll_offset);
310 scroll_offset
311 }
312
313 fn measure_item(
315 &self,
316 list_width: Option<Pixels>,
317 window: &mut Window,
318 cx: &mut App,
319 ) -> Size<Pixels> {
320 if self.items_count == 0 {
321 return Size::default();
322 }
323
324 let item_ix = self.item_to_measure_index.min(self.items_count - 1);
325 let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx);
326 let Some(mut item_to_measure) = items.pop() else {
327 return Size::default();
328 };
329 let available_space = size(
330 list_width.map_or(AvailableSpace::MinContent, |width| {
331 AvailableSpace::Definite(width)
332 }),
333 AvailableSpace::MinContent,
334 );
335 item_to_measure.layout_as_root(available_space, window, cx)
336 }
337}
338
339pub struct VirtualListFrameState {
341 items: SmallVec<[AnyElement; 32]>,
343 size_layout: ItemSizeLayout,
344}
345
346#[derive(Default, Clone)]
347pub struct ItemSizeLayout {
348 items_sizes: Rc<Vec<Size<Pixels>>>,
349 content_size: Size<Pixels>,
350 sizes: Vec<Pixels>,
351 origins: Vec<Pixels>,
352 last_layout_bounds: Bounds<Pixels>,
353}
354
355impl IntoElement for VirtualList {
356 type Element = Self;
357
358 fn into_element(self) -> Self::Element {
359 self
360 }
361}
362
363impl Element for VirtualList {
364 type RequestLayoutState = VirtualListFrameState;
365 type PrepaintState = Option<Hitbox>;
366
367 fn id(&self) -> Option<ElementId> {
368 Some(self.id.clone())
369 }
370
371 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
372 None
373 }
374
375 fn request_layout(
376 &mut self,
377 global_id: Option<&GlobalElementId>,
378 inspector_id: Option<&gpui::InspectorElementId>,
379 window: &mut Window,
380 cx: &mut App,
381 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
382 let rem_size = window.rem_size();
383 let font_size = window.text_style().font_size.to_pixels(rem_size);
384 let mut size_layout = ItemSizeLayout::default();
385 let list_width = if self.axis.is_vertical() {
390 self.scroll_handle
391 .state
392 .borrow()
393 .last_content_size
394 .map(|size| size.width)
395 .filter(|width| !width.is_zero())
396 } else {
397 None
398 };
399 let longest_item_size = self.measure_item(list_width, window, cx);
400
401 let layout_id = self.base.interactivity().request_layout(
402 global_id,
403 inspector_id,
404 window,
405 cx,
406 |style, window, cx| {
407 size_layout = window.with_element_state(
408 global_id.unwrap(),
409 |state: Option<ItemSizeLayout>, _window| {
410 let mut state = state.unwrap_or(ItemSizeLayout::default());
411
412 let gap = style
414 .gap
415 .along(self.axis)
416 .to_pixels(font_size.into(), rem_size);
417
418 if state.items_sizes != self.item_sizes {
419 state.items_sizes = self.item_sizes.clone();
420 state.sizes = self
422 .item_sizes
423 .iter()
424 .enumerate()
425 .map(|(i, size)| {
426 let size = size.along(self.axis);
427 if i + 1 == self.items_count {
428 size
429 } else {
430 size + gap
431 }
432 })
433 .collect::<Vec<_>>();
434
435 state.origins = state
437 .sizes
438 .iter()
439 .scan(px(0.), |cumulative, size| match self.axis {
440 Axis::Horizontal => {
441 let x = *cumulative;
442 *cumulative += *size;
443 Some(x)
444 }
445 Axis::Vertical => {
446 let y = *cumulative;
447 *cumulative += *size;
448 Some(y)
449 }
450 })
451 .collect::<Vec<_>>();
452
453 if self.axis.is_horizontal() {
454 state.content_size.width =
455 px(state.sizes.iter().map(|size| size.as_f32()).sum::<f32>());
456 } else {
457 state.content_size.height =
458 px(state.sizes.iter().map(|size| size.as_f32()).sum::<f32>());
459 }
460 }
461
462 if self.axis.is_horizontal() {
463 state.content_size.height = longest_item_size.height;
464 } else {
465 state.content_size.width = longest_item_size.width;
466 }
467
468 (state.clone(), state)
469 },
470 );
471
472 let axis = self.axis;
473 let layout_id =
474 match self.sizing_behavior {
475 ListSizingBehavior::Infer => {
476 window.with_text_style(style.text_style().cloned(), |window| {
477 let size_layout = size_layout.clone();
478
479 window.request_measured_layout(style, {
480 move |known_dimensions, available_space, _, _| {
481 let mut size = Size::default();
482 if axis.is_horizontal() {
483 size.width = known_dimensions.width.unwrap_or(
484 match available_space.width {
485 AvailableSpace::Definite(x) => x,
486 AvailableSpace::MinContent
487 | AvailableSpace::MaxContent => {
488 size_layout.content_size.width
489 }
490 },
491 );
492 size.height = known_dimensions.width.unwrap_or(
493 match available_space.height {
494 AvailableSpace::Definite(x) => x,
495 AvailableSpace::MinContent
496 | AvailableSpace::MaxContent => {
497 size_layout.content_size.height
498 }
499 },
500 );
501 } else {
502 size.width = known_dimensions.width.unwrap_or(
503 match available_space.width {
504 AvailableSpace::Definite(x) => x,
505 AvailableSpace::MinContent
506 | AvailableSpace::MaxContent => {
507 size_layout.content_size.width
508 }
509 },
510 );
511 size.height = known_dimensions.height.unwrap_or(
512 match available_space.height {
513 AvailableSpace::Definite(x) => x,
514 AvailableSpace::MinContent
515 | AvailableSpace::MaxContent => {
516 size_layout.content_size.height
517 }
518 },
519 );
520 }
521
522 size
523 }
524 })
525 })
526 }
527 ListSizingBehavior::Auto => window
528 .with_text_style(style.text_style().cloned(), |window| {
529 window.request_layout(style, None, cx)
530 }),
531 };
532
533 layout_id
534 },
535 );
536
537 (
538 layout_id,
539 VirtualListFrameState {
540 items: SmallVec::new(),
541 size_layout,
542 },
543 )
544 }
545
546 fn prepaint(
547 &mut self,
548 global_id: Option<&GlobalElementId>,
549 inspector_id: Option<&gpui::InspectorElementId>,
550 bounds: Bounds<Pixels>,
551 layout: &mut Self::RequestLayoutState,
552 window: &mut Window,
553 cx: &mut App,
554 ) -> Self::PrepaintState {
555 layout.size_layout.last_layout_bounds = bounds;
556
557 let style = self
558 .base
559 .interactivity()
560 .compute_style(global_id, None, window, cx);
561 let border_widths = style.border_widths.to_pixels(window.rem_size());
562 let paddings = style
563 .padding
564 .to_pixels(bounds.size.into(), window.rem_size());
565
566 let item_sizes = &layout.size_layout.sizes;
567 let item_origins = &layout.size_layout.origins;
568
569 let content_bounds = Bounds::from_corners(
570 bounds.origin
571 + point(
572 border_widths.left + paddings.left,
573 border_widths.top + paddings.top,
574 ),
575 bounds.bottom_right()
576 - point(
577 border_widths.right + paddings.right,
578 border_widths.bottom + paddings.bottom,
579 ),
580 );
581
582 let items_bounds = item_origins
584 .iter()
585 .enumerate()
586 .map(|(i, &origin)| {
587 let item_size = item_sizes[i];
588
589 Bounds {
590 origin: match self.axis {
591 Axis::Horizontal => point(content_bounds.left() + origin, px(0.)),
592 Axis::Vertical => point(px(0.), content_bounds.top() + origin),
593 },
594 size: match self.axis {
595 Axis::Horizontal => size(item_size, content_bounds.size.height),
596 Axis::Vertical => size(content_bounds.size.width, item_size),
597 },
598 }
599 })
600 .collect::<Vec<_>>();
601
602 let axis = self.axis;
603
604 let mut scroll_state = self.scroll_handle.state.borrow_mut();
605 scroll_state.axis = axis;
606 scroll_state.items_count = self.items_count;
607 scroll_state.last_content_size = Some(content_bounds.size);
608
609 let mut scroll_offset = self.scroll_handle.offset();
610 if let Some(scroll_to_item) = scroll_state.deferred_scroll_to_item.take() {
611 scroll_offset = self.scroll_to_deferred_item(
612 scroll_offset,
613 &items_bounds,
614 &content_bounds,
615 scroll_to_item,
616 );
617 }
618
619 scroll_offset = scroll_offset
620 .max(&point(
621 content_bounds.size.width - layout.size_layout.content_size.width,
622 content_bounds.size.height - layout.size_layout.content_size.height,
623 ))
624 .min(&point(px(0.), px(0.)));
625 if scroll_offset != self.scroll_handle.offset() {
626 self.scroll_handle.set_offset(scroll_offset);
627 }
628
629 self.base.interactivity().prepaint(
630 global_id,
631 inspector_id,
632 bounds,
633 layout.size_layout.content_size,
634 window,
635 cx,
636 |_style, _, hitbox, window, cx| {
637 if self.items_count > 0 {
638 let min_scroll_offset = content_bounds.size.along(self.axis)
639 - layout.size_layout.content_size.along(self.axis);
640
641 let is_scrolled = !scroll_offset.along(self.axis).is_zero();
642 if is_scrolled {
643 match self.axis {
644 Axis::Horizontal if scroll_offset.x < min_scroll_offset => {
645 scroll_offset.x = min_scroll_offset;
646 self.scroll_handle.set_offset(scroll_offset);
647 }
648 Axis::Vertical if scroll_offset.y < min_scroll_offset => {
649 scroll_offset.y = min_scroll_offset;
650 self.scroll_handle.set_offset(scroll_offset);
651 }
652 _ => {}
653 }
654 }
655
656 let (first_visible_element_ix, last_visible_element_ix) = match self.axis {
657 Axis::Horizontal => {
658 let mut cumulative_size = px(0.);
659 let mut first_visible_element_ix = 0;
660 for (i, &size) in item_sizes.iter().enumerate() {
661 cumulative_size += size;
662 if cumulative_size > -(scroll_offset.x + paddings.left) {
663 first_visible_element_ix = i;
664 break;
665 }
666 }
667
668 cumulative_size = px(0.);
669 let mut last_visible_element_ix = 0;
670 for (i, &size) in item_sizes.iter().enumerate() {
671 cumulative_size += size;
672 if cumulative_size > (-scroll_offset.x + content_bounds.size.width)
673 {
674 last_visible_element_ix = i + 1;
675 break;
676 }
677 }
678 if last_visible_element_ix == 0 {
679 last_visible_element_ix = self.items_count;
680 } else {
681 last_visible_element_ix += 1;
682 }
683 (first_visible_element_ix, last_visible_element_ix)
684 }
685 Axis::Vertical => {
686 let mut cumulative_size = px(0.);
687 let mut first_visible_element_ix = 0;
688 for (i, &size) in item_sizes.iter().enumerate() {
689 cumulative_size += size;
690 if cumulative_size > -(scroll_offset.y + paddings.top) {
691 first_visible_element_ix = i;
692 break;
693 }
694 }
695
696 cumulative_size = px(0.);
697 let mut last_visible_element_ix = 0;
698 for (i, &size) in item_sizes.iter().enumerate() {
699 cumulative_size += size;
700 if cumulative_size > (-scroll_offset.y + content_bounds.size.height)
701 {
702 last_visible_element_ix = i + 1;
703 break;
704 }
705 }
706 if last_visible_element_ix == 0 {
707 last_visible_element_ix = self.items_count;
708 } else {
709 last_visible_element_ix += 1;
710 }
711 (first_visible_element_ix, last_visible_element_ix)
712 }
713 };
714
715 let visible_range = first_visible_element_ix
716 ..cmp::min(last_visible_element_ix, self.items_count);
717
718 let items = (self.render_items)(visible_range.clone(), window, cx);
719
720 let content_mask = ContentMask { bounds };
721 window.with_content_mask(Some(content_mask), |window| {
722 for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
723 let item_origin = match self.axis {
724 Axis::Horizontal => {
725 content_bounds.origin
726 + point(item_origins[ix] + scroll_offset.x, scroll_offset.y)
727 }
728 Axis::Vertical => {
729 content_bounds.origin
730 + point(scroll_offset.x, item_origins[ix] + scroll_offset.y)
731 }
732 };
733
734 let available_space = match self.axis {
735 Axis::Horizontal => size(
736 AvailableSpace::Definite(item_sizes[ix]),
737 AvailableSpace::Definite(content_bounds.size.height),
738 ),
739 Axis::Vertical => size(
740 AvailableSpace::Definite(content_bounds.size.width),
741 AvailableSpace::Definite(item_sizes[ix]),
742 ),
743 };
744
745 item.layout_as_root(available_space, window, cx);
746 item.prepaint_at(item_origin, window, cx);
747 layout.items.push(item);
748 }
749 });
750 }
751
752 hitbox
753 },
754 )
755 }
756
757 fn paint(
758 &mut self,
759 global_id: Option<&GlobalElementId>,
760 inspector_id: Option<&gpui::InspectorElementId>,
761 bounds: Bounds<Pixels>,
762 layout: &mut Self::RequestLayoutState,
763 hitbox: &mut Self::PrepaintState,
764 window: &mut Window,
765 cx: &mut App,
766 ) {
767 self.base.interactivity().paint(
768 global_id,
769 inspector_id,
770 bounds,
771 hitbox.as_ref(),
772 window,
773 cx,
774 |_, window, cx| {
775 for item in &mut layout.items {
776 item.paint(window, cx);
777 }
778 },
779 )
780 }
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786
787 use gpui::{Context, TestAppContext};
788
789 struct VirtualListHarness {
790 axis: Axis,
791 item_sizes: Rc<Vec<Size<Pixels>>>,
792 scroll_handle: VirtualListScrollHandle,
793 visible_ranges: Rc<RefCell<Vec<Range<usize>>>>,
794 }
795
796 impl Render for VirtualListHarness {
797 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
798 let item_sizes = self.item_sizes.clone();
799 let visible_ranges = self.visible_ranges.clone();
800 virtual_list(
801 cx.entity(),
802 "virtual-list-test",
803 self.axis,
804 self.item_sizes.clone(),
805 move |_, visible_range, _, _| {
806 visible_ranges.borrow_mut().push(visible_range.clone());
807 visible_range
808 .map(|ix| div().w(item_sizes[ix].width).h(item_sizes[ix].height))
809 .collect::<Vec<_>>()
810 },
811 )
812 .track_scroll(&self.scroll_handle)
813 .w(px(60.))
814 .h(px(60.))
815 }
816 }
817
818 fn exercise_axis(cx: &mut TestAppContext, axis: Axis) {
819 let item_size = match axis {
820 Axis::Horizontal => size(px(20.), px(10.)),
821 Axis::Vertical => size(px(10.), px(20.)),
822 };
823 let item_sizes = Rc::new(vec![item_size; 20]);
824 let scroll_handle = VirtualListScrollHandle::new();
825 let visible_ranges = Rc::new(RefCell::new(Vec::new()));
826 let (_, cx) = cx.add_window_view({
827 let item_sizes = item_sizes.clone();
828 let scroll_handle = scroll_handle.clone();
829 let visible_ranges = visible_ranges.clone();
830 move |_, _| VirtualListHarness {
831 axis,
832 item_sizes,
833 scroll_handle,
834 visible_ranges,
835 }
836 });
837
838 cx.update(|window, cx| window.draw(cx).clear(cx));
839 let initial_range = visible_ranges.borrow().last().cloned().unwrap();
840 assert_eq!(initial_range.start, 0);
841 assert!(initial_range.end < item_sizes.len());
842
843 scroll_handle.scroll_to_item(12, ScrollStrategy::Top);
844 cx.update(|window, cx| window.draw(cx).clear(cx));
845 let scrolled_range = visible_ranges.borrow().last().cloned().unwrap();
846 assert!(scrolled_range.contains(&12));
847 match axis {
848 Axis::Horizontal => assert!(scroll_handle.offset().x < px(0.)),
849 Axis::Vertical => assert!(scroll_handle.offset().y < px(0.)),
850 }
851 }
852
853 #[gpui::test]
854 fn vertical_visible_range_and_deferred_scroll_are_preserved(cx: &mut TestAppContext) {
855 exercise_axis(cx, Axis::Vertical);
856 }
857
858 #[gpui::test]
859 fn horizontal_visible_range_and_deferred_scroll_are_preserved(cx: &mut TestAppContext) {
860 exercise_axis(cx, Axis::Horizontal);
861 }
862
863 #[gpui::test]
864 fn empty_list_draws_without_requesting_items(cx: &mut TestAppContext) {
865 let visible_ranges = Rc::new(RefCell::new(Vec::new()));
866 let (_, cx) = cx.add_window_view({
867 let visible_ranges = visible_ranges.clone();
868 move |_, _| VirtualListHarness {
869 axis: Axis::Vertical,
870 item_sizes: Rc::new(Vec::new()),
871 scroll_handle: VirtualListScrollHandle::new(),
872 visible_ranges,
873 }
874 });
875
876 cx.update(|window, cx| window.draw(cx).clear(cx));
877 assert!(visible_ranges.borrow().is_empty());
878 }
879}