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 axis_and_gap: Option<(Axis, Pixels)>,
354 content_size: Size<Pixels>,
355 sizes: Rc<[Pixels]>,
356 origins: Rc<[Pixels]>,
357 last_layout_bounds: Bounds<Pixels>,
358}
359
360impl ItemSizeLayout {
361 fn item_bounds(
364 &self,
365 ix: usize,
366 axis: Axis,
367 content_bounds: &Bounds<Pixels>,
368 ) -> Option<Bounds<Pixels>> {
369 let origin = *self.origins.get(ix)?;
370 let item_size = self.sizes[ix];
371 Some(match axis {
372 Axis::Horizontal => Bounds {
373 origin: point(content_bounds.left() + origin, px(0.)),
374 size: size(item_size, content_bounds.size.height),
375 },
376 Axis::Vertical => Bounds {
377 origin: point(px(0.), content_bounds.top() + origin),
378 size: size(content_bounds.size.width, item_size),
379 },
380 })
381 }
382}
383
384fn visible_range(origins: &[Pixels], sizes: &[Pixels], viewport: Range<Pixels>) -> Range<usize> {
389 let count = origins.len();
390 let ends_before = |edge: Pixels| {
393 let mut low = 0;
394 let mut high = count;
395 while low < high {
396 let mid = low + (high - low) / 2;
397 if origins[mid] + sizes[mid] <= edge {
398 low = mid + 1;
399 } else {
400 high = mid;
401 }
402 }
403 low
404 };
405 let first = ends_before(viewport.start);
406 let past_end = ends_before(viewport.end);
407 let last = if past_end == count {
408 count
409 } else {
410 (past_end + 2).min(count)
411 };
412 first..last.max(first)
413}
414
415impl IntoElement for VirtualList {
416 type Element = Self;
417
418 fn into_element(self) -> Self::Element {
419 self
420 }
421}
422
423impl Element for VirtualList {
424 type RequestLayoutState = VirtualListFrameState;
425 type PrepaintState = Option<Hitbox>;
426
427 fn id(&self) -> Option<ElementId> {
428 Some(self.id.clone())
429 }
430
431 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
432 None
433 }
434
435 fn request_layout(
436 &mut self,
437 global_id: Option<&GlobalElementId>,
438 inspector_id: Option<&gpui::InspectorElementId>,
439 window: &mut Window,
440 cx: &mut App,
441 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
442 let rem_size = window.rem_size();
443 let font_size = window.text_style().font_size.to_pixels(rem_size);
444 let mut size_layout = ItemSizeLayout::default();
445 let list_width = if self.axis.is_vertical() {
450 self.scroll_handle
451 .state
452 .borrow()
453 .last_content_size
454 .map(|size| size.width)
455 .filter(|width| !width.is_zero())
456 } else {
457 None
458 };
459 let longest_item_size = self.measure_item(list_width, window, cx);
460
461 let layout_id = self.base.interactivity().request_layout(
462 global_id,
463 inspector_id,
464 window,
465 cx,
466 |style, window, cx| {
467 size_layout = window.with_element_state(
468 global_id.unwrap(),
469 |state: Option<ItemSizeLayout>, _window| {
470 let mut state = state.unwrap_or(ItemSizeLayout::default());
471
472 let gap = style
474 .gap
475 .along(self.axis)
476 .to_pixels(font_size.into(), rem_size);
477
478 if state.items_sizes != self.item_sizes
479 || state.axis_and_gap != Some((self.axis, gap))
480 {
481 state.items_sizes = self.item_sizes.clone();
482 state.axis_and_gap = Some((self.axis, gap));
483 state.sizes = self
485 .item_sizes
486 .iter()
487 .enumerate()
488 .map(|(i, size)| {
489 let size = size.along(self.axis);
490 if i + 1 == self.items_count {
491 size
492 } else {
493 size + gap
494 }
495 })
496 .collect();
497
498 let mut cumulative = px(0.);
500 state.origins = state
501 .sizes
502 .iter()
503 .map(|size| {
504 let origin = cumulative;
505 cumulative += *size;
506 origin
507 })
508 .collect();
509
510 if self.axis.is_horizontal() {
511 state.content_size.width = cumulative;
512 } else {
513 state.content_size.height = cumulative;
514 }
515 }
516
517 if self.axis.is_horizontal() {
518 state.content_size.height = longest_item_size.height;
519 } else {
520 state.content_size.width = longest_item_size.width;
521 }
522
523 (state.clone(), state)
524 },
525 );
526
527 let axis = self.axis;
528 let layout_id =
529 match self.sizing_behavior {
530 ListSizingBehavior::Infer => {
531 window.with_text_style(style.text_style().cloned(), |window| {
532 let size_layout = size_layout.clone();
533
534 window.request_measured_layout(style, {
535 move |known_dimensions, available_space, _, _| {
536 let mut size = Size::default();
537 if axis.is_horizontal() {
538 size.width = known_dimensions.width.unwrap_or(
539 match available_space.width {
540 AvailableSpace::Definite(x) => x,
541 AvailableSpace::MinContent
542 | AvailableSpace::MaxContent => {
543 size_layout.content_size.width
544 }
545 },
546 );
547 size.height = known_dimensions.height.unwrap_or(
548 match available_space.height {
549 AvailableSpace::Definite(x) => x,
550 AvailableSpace::MinContent
551 | AvailableSpace::MaxContent => {
552 size_layout.content_size.height
553 }
554 },
555 );
556 } else {
557 size.width = known_dimensions.width.unwrap_or(
558 match available_space.width {
559 AvailableSpace::Definite(x) => x,
560 AvailableSpace::MinContent
561 | AvailableSpace::MaxContent => {
562 size_layout.content_size.width
563 }
564 },
565 );
566 size.height = known_dimensions.height.unwrap_or(
567 match available_space.height {
568 AvailableSpace::Definite(x) => x,
569 AvailableSpace::MinContent
570 | AvailableSpace::MaxContent => {
571 size_layout.content_size.height
572 }
573 },
574 );
575 }
576
577 size
578 }
579 })
580 })
581 }
582 ListSizingBehavior::Auto => window
583 .with_text_style(style.text_style().cloned(), |window| {
584 window.request_layout(style, None, cx)
585 }),
586 };
587
588 layout_id
589 },
590 );
591
592 (
593 layout_id,
594 VirtualListFrameState {
595 items: SmallVec::new(),
596 size_layout,
597 },
598 )
599 }
600
601 fn prepaint(
602 &mut self,
603 global_id: Option<&GlobalElementId>,
604 inspector_id: Option<&gpui::InspectorElementId>,
605 bounds: Bounds<Pixels>,
606 layout: &mut Self::RequestLayoutState,
607 window: &mut Window,
608 cx: &mut App,
609 ) -> Self::PrepaintState {
610 layout.size_layout.last_layout_bounds = bounds;
611
612 let style = self
613 .base
614 .interactivity()
615 .compute_style(global_id, None, window, cx);
616 let border_widths = style.border_widths.to_pixels(window.rem_size());
617 let paddings = style
618 .padding
619 .to_pixels(bounds.size.into(), window.rem_size());
620
621 let item_sizes = &layout.size_layout.sizes;
622 let item_origins = &layout.size_layout.origins;
623
624 let content_bounds = Bounds::from_corners(
625 bounds.origin
626 + point(
627 border_widths.left + paddings.left,
628 border_widths.top + paddings.top,
629 ),
630 bounds.bottom_right()
631 - point(
632 border_widths.right + paddings.right,
633 border_widths.bottom + paddings.bottom,
634 ),
635 );
636
637 let axis = self.axis;
638
639 let mut scroll_state = self.scroll_handle.state.borrow_mut();
640 scroll_state.axis = axis;
641 scroll_state.items_count = self.items_count;
642 scroll_state.last_content_size = Some(content_bounds.size);
643
644 let mut scroll_offset = self.scroll_handle.offset();
645 if let Some(scroll_to_item) = scroll_state.deferred_scroll_to_item.take() {
646 scroll_offset = self.scroll_to_deferred_item(
647 scroll_offset,
648 &layout.size_layout,
649 &content_bounds,
650 scroll_to_item,
651 );
652 }
653
654 scroll_offset = scroll_offset
655 .max(&point(
656 content_bounds.size.width - layout.size_layout.content_size.width,
657 content_bounds.size.height - layout.size_layout.content_size.height,
658 ))
659 .min(&point(px(0.), px(0.)));
660 if scroll_offset != self.scroll_handle.offset() {
661 self.scroll_handle.set_offset(scroll_offset);
662 }
663
664 self.base.interactivity().prepaint(
665 global_id,
666 inspector_id,
667 bounds,
668 layout.size_layout.content_size,
669 window,
670 cx,
671 |_style, _, hitbox, window, cx| {
672 if self.items_count > 0 {
673 let min_scroll_offset = content_bounds.size.along(self.axis)
674 - layout.size_layout.content_size.along(self.axis);
675
676 let is_scrolled = !scroll_offset.along(self.axis).is_zero();
677 if is_scrolled {
678 match self.axis {
679 Axis::Horizontal if scroll_offset.x < min_scroll_offset => {
680 scroll_offset.x = min_scroll_offset;
681 self.scroll_handle.set_offset(scroll_offset);
682 }
683 Axis::Vertical if scroll_offset.y < min_scroll_offset => {
684 scroll_offset.y = min_scroll_offset;
685 self.scroll_handle.set_offset(scroll_offset);
686 }
687 _ => {}
688 }
689 }
690
691 let viewport = match self.axis {
695 Axis::Horizontal => {
696 -(scroll_offset.x + paddings.left)
697 ..-scroll_offset.x + content_bounds.size.width
698 }
699 Axis::Vertical => {
700 -(scroll_offset.y + paddings.top)
701 ..-scroll_offset.y + content_bounds.size.height
702 }
703 };
704 let visible_range = visible_range(item_origins, item_sizes, viewport);
705
706 let items = (self.render_items)(visible_range.clone(), window, cx);
707
708 let content_mask = ContentMask { bounds };
709 window.with_content_mask(Some(content_mask), |window| {
710 for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
711 let item_origin = match self.axis {
712 Axis::Horizontal => {
713 content_bounds.origin
714 + point(item_origins[ix] + scroll_offset.x, scroll_offset.y)
715 }
716 Axis::Vertical => {
717 content_bounds.origin
718 + point(scroll_offset.x, item_origins[ix] + scroll_offset.y)
719 }
720 };
721
722 let available_space = match self.axis {
723 Axis::Horizontal => size(
724 AvailableSpace::Definite(item_sizes[ix]),
725 AvailableSpace::Definite(content_bounds.size.height),
726 ),
727 Axis::Vertical => size(
728 AvailableSpace::Definite(content_bounds.size.width),
729 AvailableSpace::Definite(item_sizes[ix]),
730 ),
731 };
732
733 item.layout_as_root(available_space, window, cx);
734 item.prepaint_at(item_origin, window, cx);
735 layout.items.push(item);
736 }
737 });
738 }
739
740 hitbox
741 },
742 )
743 }
744
745 fn paint(
746 &mut self,
747 global_id: Option<&GlobalElementId>,
748 inspector_id: Option<&gpui::InspectorElementId>,
749 bounds: Bounds<Pixels>,
750 layout: &mut Self::RequestLayoutState,
751 hitbox: &mut Self::PrepaintState,
752 window: &mut Window,
753 cx: &mut App,
754 ) {
755 self.base.interactivity().paint(
756 global_id,
757 inspector_id,
758 bounds,
759 hitbox.as_ref(),
760 window,
761 cx,
762 |_, window, cx| {
763 for item in &mut layout.items {
764 item.paint(window, cx);
765 }
766 },
767 )
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774
775 use gpui::{Context, ParentElement as _, TestAppContext, VisualTestContext};
776
777 struct VirtualListHarness {
778 axis: Axis,
779 item_sizes: Rc<Vec<Size<Pixels>>>,
780 scroll_handle: VirtualListScrollHandle,
781 visible_ranges: Rc<RefCell<Vec<Range<usize>>>>,
782 }
783
784 impl Render for VirtualListHarness {
785 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
786 let item_sizes = self.item_sizes.clone();
787 let visible_ranges = self.visible_ranges.clone();
788 virtual_list(
789 cx.entity(),
790 "virtual-list-test",
791 self.axis,
792 self.item_sizes.clone(),
793 move |_, visible_range, _, _| {
794 visible_ranges.borrow_mut().push(visible_range.clone());
795 visible_range
796 .map(|ix| div().w(item_sizes[ix].width).h(item_sizes[ix].height))
797 .collect::<Vec<_>>()
798 },
799 )
800 .track_scroll(&self.scroll_handle)
801 .w(px(60.))
802 .h(px(60.))
803 }
804 }
805
806 fn exercise_axis(cx: &mut TestAppContext, axis: Axis) {
807 let item_size = match axis {
808 Axis::Horizontal => size(px(20.), px(10.)),
809 Axis::Vertical => size(px(10.), px(20.)),
810 };
811 let item_sizes = Rc::new(vec![item_size; 20]);
812 let scroll_handle = VirtualListScrollHandle::new();
813 let visible_ranges = Rc::new(RefCell::new(Vec::new()));
814 let (_, cx) = cx.add_window_view({
815 let item_sizes = item_sizes.clone();
816 let scroll_handle = scroll_handle.clone();
817 let visible_ranges = visible_ranges.clone();
818 move |_, _| VirtualListHarness {
819 axis,
820 item_sizes,
821 scroll_handle,
822 visible_ranges,
823 }
824 });
825
826 cx.update(|window, cx| window.draw(cx).clear(cx));
827 let initial_range = visible_ranges.borrow().last().cloned().unwrap();
828 assert_eq!(initial_range.start, 0);
829 assert!(initial_range.end < item_sizes.len());
830
831 scroll_handle.scroll_to_item(12, ScrollStrategy::Top);
832 cx.update(|window, cx| window.draw(cx).clear(cx));
833 let scrolled_range = visible_ranges.borrow().last().cloned().unwrap();
834 assert!(scrolled_range.contains(&12));
835 match axis {
836 Axis::Horizontal => assert!(scroll_handle.offset().x < px(0.)),
837 Axis::Vertical => assert!(scroll_handle.offset().y < px(0.)),
838 }
839 }
840
841 fn layout(sizes: &[f32]) -> (Vec<Pixels>, Vec<Pixels>) {
842 let sizes: Vec<Pixels> = sizes.iter().map(|size| px(*size)).collect();
843 let origins = sizes
844 .iter()
845 .scan(px(0.), |cumulative, size| {
846 let origin = *cumulative;
847 *cumulative += *size;
848 Some(origin)
849 })
850 .collect();
851 (origins, sizes)
852 }
853
854 #[test]
855 fn visible_range_starts_at_the_first_item_crossing_the_viewport() {
856 let (origins, sizes) = layout(&[20.; 10]);
857 assert_eq!(visible_range(&origins, &sizes, px(0.)..px(60.)), 0..5);
858 assert_eq!(visible_range(&origins, &sizes, px(50.)..px(110.)), 2..7);
859 }
860
861 #[test]
862 fn visible_range_overdraws_one_item_past_the_viewport_but_not_past_the_end() {
863 let (origins, sizes) = layout(&[20.; 10]);
864 assert_eq!(visible_range(&origins, &sizes, px(150.)..px(210.)), 7..10);
865 assert_eq!(visible_range(&origins, &sizes, px(0.)..px(500.)), 0..10);
866 }
867
868 #[test]
869 fn visible_range_handles_uneven_sizes() {
870 let (origins, sizes) = layout(&[10., 30., 5., 50.]);
871 assert_eq!(visible_range(&origins, &sizes, px(12.)..px(44.)), 1..4);
872 assert_eq!(visible_range(&origins, &sizes, px(45.)..px(50.)), 3..4);
873 }
874
875 #[test]
876 fn visible_range_is_empty_without_items_or_past_the_content() {
877 let (origins, sizes) = layout(&[]);
878 assert_eq!(visible_range(&origins, &sizes, px(0.)..px(60.)), 0..0);
879 let (origins, sizes) = layout(&[20.; 10]);
880 assert!(visible_range(&origins, &sizes, px(300.)..px(360.)).is_empty());
881 }
882
883 #[gpui::test]
884 fn vertical_visible_range_and_deferred_scroll_are_preserved(cx: &mut TestAppContext) {
885 exercise_axis(cx, Axis::Vertical);
886 }
887
888 #[gpui::test]
889 fn horizontal_visible_range_and_deferred_scroll_are_preserved(cx: &mut TestAppContext) {
890 exercise_axis(cx, Axis::Horizontal);
891 }
892
893 struct GeometryHarness {
895 axis: Axis,
896 gap: Pixels,
897 }
898
899 impl Render for GeometryHarness {
900 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
901 virtual_list(
902 cx.entity(),
903 "virtual-list-geometry",
904 self.axis,
905 Rc::new(vec![size(px(30.), px(10.)); 3]),
906 |_, visible_range, _, _| {
907 visible_range
908 .map(|ix| {
909 div()
910 .w(px(30.))
911 .h(px(10.))
912 .debug_selector(move || format!("geometry-item-{ix}"))
913 })
914 .collect::<Vec<_>>()
915 },
916 )
917 .gap(self.gap)
918 .w(px(120.))
919 .h(px(100.))
920 }
921 }
922
923 fn second_item_offset(cx: &mut VisualTestContext) -> Point<Pixels> {
925 let first = cx.debug_bounds("geometry-item-0").unwrap().origin;
926 let second = cx.debug_bounds("geometry-item-1").unwrap().origin;
927 second - first
928 }
929
930 #[gpui::test]
931 fn changing_the_gap_moves_the_items(cx: &mut TestAppContext) {
932 let (view, cx) = cx.add_window_view(|_, _| GeometryHarness {
933 axis: Axis::Vertical,
934 gap: px(0.),
935 });
936 cx.update(|window, cx| window.draw(cx).clear(cx));
937 assert_eq!(second_item_offset(cx), point(px(0.), px(10.)));
938
939 cx.update(|window, cx| {
940 view.update(cx, |harness, cx| {
941 harness.gap = px(20.);
942 cx.notify();
943 });
944 window.draw(cx).clear(cx);
945 });
946 assert_eq!(second_item_offset(cx), point(px(0.), px(30.)));
947 }
948
949 #[gpui::test]
950 fn changing_the_axis_moves_the_items(cx: &mut TestAppContext) {
951 let (view, cx) = cx.add_window_view(|_, _| GeometryHarness {
952 axis: Axis::Vertical,
953 gap: px(0.),
954 });
955 cx.update(|window, cx| window.draw(cx).clear(cx));
956 assert_eq!(second_item_offset(cx), point(px(0.), px(10.)));
957
958 cx.update(|window, cx| {
959 view.update(cx, |harness, cx| {
960 harness.axis = Axis::Horizontal;
961 cx.notify();
962 });
963 window.draw(cx).clear(cx);
964 });
965 assert_eq!(second_item_offset(cx), point(px(30.), px(0.)));
966 }
967
968 #[gpui::test]
969 fn a_horizontal_list_infers_its_height_from_its_items(cx: &mut TestAppContext) {
970 struct InferHarness;
971
972 impl Render for InferHarness {
973 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
974 div()
975 .flex()
976 .flex_col()
977 .items_start()
978 .w(px(120.))
979 .child(
980 virtual_list(
981 cx.entity(),
982 "virtual-list-infer",
983 Axis::Horizontal,
984 Rc::new(vec![size(px(30.), px(10.)); 3]),
985 |_, visible_range, _, _| {
986 visible_range
987 .map(|_| div().w(px(30.)).h(px(10.)))
988 .collect::<Vec<_>>()
989 },
990 )
991 .with_sizing_behavior(ListSizingBehavior::Infer)
992 .w(px(120.))
993 .h_auto()
994 .flex_none(),
995 )
996 .child(div().size(px(1.)).debug_selector(|| "after-list".into()))
997 }
998 }
999
1000 let (_, cx) = cx.add_window_view(|_, _| InferHarness);
1001 cx.update(|window, cx| window.draw(cx).clear(cx));
1002 assert_eq!(cx.debug_bounds("after-list").unwrap().top(), px(10.));
1004 }
1005
1006 #[gpui::test]
1007 fn empty_list_draws_without_requesting_items(cx: &mut TestAppContext) {
1008 let visible_ranges = Rc::new(RefCell::new(Vec::new()));
1009 let (_, cx) = cx.add_window_view({
1010 let visible_ranges = visible_ranges.clone();
1011 move |_, _| VirtualListHarness {
1012 axis: Axis::Vertical,
1013 item_sizes: Rc::new(Vec::new()),
1014 scroll_handle: VirtualListScrollHandle::new(),
1015 visible_ranges,
1016 }
1017 });
1018
1019 cx.update(|window, cx| window.draw(cx).clear(cx));
1020 assert!(visible_ranges.borrow().is_empty());
1021 }
1022}