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