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    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    /// Content bounds size (excluding padding and border) from the last frame.
34    last_content_size: Option<Size<Pixels>>,
35    pub deferred_scroll_to_item: Option<DeferredScrollToItem>,
36}
37
38/// A scroll handle for [`VirtualList`].
39///
40/// See also [`ScrollHandle`].
41#[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    /// Create a new VirtualListScrollHandle.
89    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    /// Get the base scroll handle.
102    pub fn base_handle(&self) -> &ScrollHandle {
103        &self.base_handle
104    }
105
106    /// Scroll to the item at the given index.
107    pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
108        self.scroll_to_item_with_offset(ix, strategy, 0);
109    }
110
111    /// Scroll to the item at the given index, with an additional offset items.
112    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    /// Scrolls to the bottom of the list.
123    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/// Create a [`VirtualList`] in vertical direction.
130///
131/// This is like `uniform_list` in GPUI, but support two axis.
132///
133/// The `item_sizes` is the size of each row. Only the `height` is used; `width` is inferred
134/// by measuring the item selected with [`VirtualList::with_item_to_measure_index`], defaulting
135/// to the first item.
136///
137/// See also [`h_virtual_list`]
138#[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/// Create a [`VirtualList`] in horizontal direction.
153///
154/// The `item_sizes` is the size of each column. Only the `width` is used; `height` is inferred
155/// by measuring the item selected with [`VirtualList::with_item_to_measure_index`], defaulting
156/// to the first item.
157///
158/// See also [`v_virtual_list`]
159#[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
214/// VirtualList component for rendering a large number of differently sized items.
215pub 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    /// Set the sizing behavior for the list.
243    pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
244        self.sizing_behavior = behavior;
245        self
246    }
247
248    /// Set the item index used to infer the list's cross-axis size.
249    pub fn with_item_to_measure_index(mut self, index: usize) -> Self {
250        self.item_to_measure_index = index;
251        self
252    }
253
254    /// Specify for table.
255    ///
256    /// Table is special, because the `scroll_handle` is based on Table head (That is not a virtual list).
257    #[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                // 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/// Per-item sizes along the list axis, gap included, and their prefix sums.
347///
348/// Shared between the element state and the frame state so that carrying
349/// them across a frame is a reference count, not a copy of every item.
350#[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    /// The bounds of item `ix` in the list's content space: offset from the
361    /// content origin along the list axis, filling it on the other.
362    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
383/// The items that intersect `viewport` along the list axis, plus one item of
384/// overdraw past its end, given each item's origin and size (gap included).
385///
386/// Both edges are binary searches.
387fn visible_range(origins: &[Pixels], sizes: &[Pixels], viewport: Range<Pixels>) -> Range<usize> {
388    let count = origins.len();
389    // Item ends grow with the index, so the first item ending past `edge` is
390    // a partition point over the indices.
391    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        // Measure vertical lists at the last known content width so relative
445        // widths and text truncation resolve like the visible rows; an
446        // unconstrained measure would turn one long non-wrapping text into a
447        // phantom horizontal scroll range.
448        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                        // Including the gap between items for calculate the item size
472                        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                            // Prepare each item's size by axis
480                            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                            // Prepare each item's origin by axis
495                            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                    // The viewport in content space: the scroll offset is
688                    // negative, and the leading padding is admitted at the
689                    // start so an item under it is still drawn.
690                    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}