Skip to main content

gpui_base/
virtual_list.rs

1//! Virtual List for render a large number of differently sized rows/columns.
2//!
3//! > NOTE: This must ensure each column width or row height.
4//!
5//! Only visible range are rendered for performance reasons.
6//!
7//! Inspired by `gpui::uniform_list`.
8//! https://github.com/zed-industries/zed/blob/0ae1603610ab6b265bdfbee7b8dbc23c5ab06edc/crates/gpui/src/elements/uniform_list.rs
9//!
10//! Unlike the `uniform_list`, the each item can have different size.
11//!
12//! This is useful for more complex layout, for example, a table with different row height.
13use 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    /// Content bounds size (excluding padding and border) from the last frame.
35    last_content_size: Option<Size<Pixels>>,
36    pub deferred_scroll_to_item: Option<DeferredScrollToItem>,
37}
38
39/// A scroll handle for [`VirtualList`].
40///
41/// See also [`ScrollHandle`].
42#[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    /// Create a new VirtualListScrollHandle.
90    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    /// Get the base scroll handle.
103    pub fn base_handle(&self) -> &ScrollHandle {
104        &self.base_handle
105    }
106
107    /// Scroll to the item at the given index.
108    pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
109        self.scroll_to_item_with_offset(ix, strategy, 0);
110    }
111
112    /// Scroll to the item at the given index, with an additional offset items.
113    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    /// Scrolls to the bottom of the list.
124    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/// Create a [`VirtualList`] in vertical direction.
131///
132/// This is like `uniform_list` in GPUI, but support two axis.
133///
134/// The `item_sizes` is the size of each row. Only the `height` is used; `width` is inferred
135/// by measuring the item selected with [`VirtualList::with_item_to_measure_index`], defaulting
136/// to the first item.
137///
138/// See also [`h_virtual_list`]
139#[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/// Create a [`VirtualList`] in horizontal direction.
154///
155/// The `item_sizes` is the size of each column. Only the `width` is used; `height` is inferred
156/// by measuring the item selected with [`VirtualList::with_item_to_measure_index`], defaulting
157/// to the first item.
158///
159/// See also [`v_virtual_list`]
160#[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
215/// VirtualList component for rendering a large number of differently sized items.
216pub 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    /// Set the sizing behavior for the list.
244    pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
245        self.sizing_behavior = behavior;
246        self
247    }
248
249    /// Set the item index used to infer the list's cross-axis size.
250    pub fn with_item_to_measure_index(mut self, index: usize) -> Self {
251        self.item_to_measure_index = index;
252        self
253    }
254
255    /// Specify for table.
256    ///
257    /// Table is special, because the `scroll_handle` is based on Table head (That is not a virtual list).
258    #[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                // Ref: https://github.com/zed-industries/zed/blob/0d145289e0867a8d5d63e5e1397a5ca69c9d49c3/crates/gpui/src/elements/div.rs#L3026
294                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    /// Ref from: https://github.com/zed-industries/zed/blob/83f9f9d9e3f5914392cab9a09e3472711a1d7b38/crates/gpui/src/elements/uniform_list.rs#L660
314    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
339/// Frame state used by the [VirtualItem].
340pub struct VirtualListFrameState {
341    /// Visible items to be painted.
342    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        // Measure vertical lists at the last known content width so relative
386        // widths and text truncation resolve like the visible rows; an
387        // unconstrained measure would turn one long non-wrapping text into a
388        // phantom horizontal scroll range.
389        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                        // Including the gap between items for calculate the item size
413                        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                            // Prepare each item's size by axis
421                            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                            // Prepare each item's origin by axis
436                            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        // Update scroll_handle with the item bounds
583        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}