gpui_component/
virtual_list.rs

1//! Vistual 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    div, point, px, size, Along, AnyElement, App, AvailableSpace, Axis, Bounds, ContentMask,
22    Context, 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,
26};
27use smallvec::SmallVec;
28
29use crate::{scroll::ScrollHandleOffsetable, AxisExt, PixelsExt};
30
31struct VirtualListScrollHandleState {
32    axis: Axis,
33    items_count: usize,
34    pub deferred_scroll_to_item: Option<DeferredScrollToItem>,
35}
36
37#[derive(Clone)]
38pub struct VirtualListScrollHandle {
39    state: Rc<RefCell<VirtualListScrollHandleState>>,
40    base_handle: ScrollHandle,
41}
42
43impl From<ScrollHandle> for VirtualListScrollHandle {
44    fn from(handle: ScrollHandle) -> Self {
45        let mut this = VirtualListScrollHandle::new();
46        this.base_handle = handle;
47        this
48    }
49}
50
51impl AsRef<ScrollHandle> for VirtualListScrollHandle {
52    fn as_ref(&self) -> &ScrollHandle {
53        &self.base_handle
54    }
55}
56
57impl ScrollHandleOffsetable for VirtualListScrollHandle {
58    fn offset(&self) -> Point<Pixels> {
59        self.base_handle.offset()
60    }
61
62    fn set_offset(&self, offset: Point<Pixels>) {
63        self.base_handle.set_offset(offset);
64    }
65
66    fn content_size(&self) -> Size<Pixels> {
67        self.base_handle.content_size()
68    }
69}
70
71impl Deref for VirtualListScrollHandle {
72    type Target = ScrollHandle;
73
74    fn deref(&self) -> &Self::Target {
75        &self.base_handle
76    }
77}
78
79impl VirtualListScrollHandle {
80    pub fn new() -> Self {
81        VirtualListScrollHandle {
82            state: Rc::new(RefCell::new(VirtualListScrollHandleState {
83                axis: Axis::Vertical,
84                items_count: 0,
85                deferred_scroll_to_item: None,
86            })),
87            base_handle: ScrollHandle::default(),
88        }
89    }
90
91    pub fn base_handle(&self) -> &ScrollHandle {
92        &self.base_handle
93    }
94
95    /// Scroll to the item at the given index.
96    pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
97        self.scroll_to_item_with_offset(ix, strategy, 0);
98    }
99
100    /// Scroll to the item at the given index, with an additional offset items.
101    fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) {
102        let mut state = self.state.borrow_mut();
103        state.deferred_scroll_to_item = Some(DeferredScrollToItem {
104            item_index: ix,
105            strategy,
106            offset,
107            scroll_strict: false,
108        });
109    }
110
111    /// Scrolls to the bottom of the list.
112    pub fn scroll_to_bottom(&self) {
113        let items_count = self.state.borrow().items_count;
114        self.scroll_to_item(items_count.saturating_sub(1), ScrollStrategy::Top);
115    }
116}
117
118/// Create a [`VirtualList`] in vertical direction.
119///
120/// This is like `uniform_list` in GPUI, but support two axis.
121///
122/// The `item_sizes` is the size of each column.
123///
124/// See also [`h_virtual_list`]
125#[inline]
126pub fn v_virtual_list<R, V>(
127    view: Entity<V>,
128    id: impl Into<ElementId>,
129    item_sizes: Rc<Vec<Size<Pixels>>>,
130    f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
131) -> VirtualList
132where
133    R: IntoElement,
134    V: Render,
135{
136    virtual_list(view, id, Axis::Vertical, item_sizes, f)
137}
138
139/// Create a [`VirtualList`] in horizontal direction.
140///
141/// See also [`v_virtual_list`]
142#[inline]
143pub fn h_virtual_list<R, V>(
144    view: Entity<V>,
145    id: impl Into<ElementId>,
146    item_sizes: Rc<Vec<Size<Pixels>>>,
147    f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
148) -> VirtualList
149where
150    R: IntoElement,
151    V: Render,
152{
153    virtual_list(view, id, Axis::Horizontal, item_sizes, f)
154}
155
156pub(crate) fn virtual_list<R, V>(
157    view: Entity<V>,
158    id: impl Into<ElementId>,
159    axis: Axis,
160    item_sizes: Rc<Vec<Size<Pixels>>>,
161    f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
162) -> VirtualList
163where
164    R: IntoElement,
165    V: Render,
166{
167    let id: ElementId = id.into();
168    let scroll_handle = VirtualListScrollHandle::new();
169    let render_range = move |visible_range, window: &mut Window, cx: &mut App| {
170        view.update(cx, |this, cx| {
171            f(this, visible_range, window, cx)
172                .into_iter()
173                .map(|component| component.into_any_element())
174                .collect()
175        })
176    };
177
178    VirtualList {
179        id: id.clone(),
180        axis,
181        base: div()
182            .id(id)
183            .size_full()
184            .overflow_scroll()
185            .track_scroll(&scroll_handle),
186        scroll_handle,
187        items_count: item_sizes.len(),
188        item_sizes,
189        render_items: Box::new(render_range),
190        sizing_behavior: ListSizingBehavior::default(),
191    }
192}
193
194/// VirtualList component for rendering a large number of differently sized items.
195pub struct VirtualList {
196    id: ElementId,
197    axis: Axis,
198    base: Stateful<Div>,
199    scroll_handle: VirtualListScrollHandle,
200    items_count: usize,
201    item_sizes: Rc<Vec<Size<Pixels>>>,
202    render_items: Box<
203        dyn for<'a> Fn(Range<usize>, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>,
204    >,
205    sizing_behavior: ListSizingBehavior,
206}
207
208impl Styled for VirtualList {
209    fn style(&mut self) -> &mut StyleRefinement {
210        self.base.style()
211    }
212}
213
214impl VirtualList {
215    pub fn track_scroll(mut self, scroll_handle: &VirtualListScrollHandle) -> Self {
216        self.base = self.base.track_scroll(&scroll_handle);
217        self.scroll_handle = scroll_handle.clone();
218        self
219    }
220
221    /// Set the sizing behavior for the list.
222    pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
223        self.sizing_behavior = behavior;
224        self
225    }
226
227    /// Specify for table.
228    ///
229    /// Table is special, because the `scroll_handle` is based on Table head (That is not a virtual list).
230    pub(crate) fn with_scroll_handle(mut self, scroll_handle: &VirtualListScrollHandle) -> Self {
231        self.base = div().id(self.id.clone()).size_full();
232        self.scroll_handle = scroll_handle.clone();
233        self
234    }
235
236    fn scroll_to_deferred_item(
237        &self,
238        scroll_offset: Point<Pixels>,
239        items_bounds: &[Bounds<Pixels>],
240        content_bounds: &Bounds<Pixels>,
241        scroll_to_item: DeferredScrollToItem,
242    ) -> Point<Pixels> {
243        let Some(bounds) = items_bounds
244            .get(scroll_to_item.item_index + scroll_to_item.offset)
245            .cloned()
246        else {
247            return scroll_offset;
248        };
249
250        let mut scroll_offset = scroll_offset;
251        match scroll_to_item.strategy {
252            ScrollStrategy::Center => {
253                if self.axis.is_vertical() {
254                    scroll_offset.y = content_bounds.top() + content_bounds.size.height.half()
255                        - bounds.top()
256                        - bounds.size.height.half()
257                } else {
258                    scroll_offset.x = content_bounds.left() + content_bounds.size.width.half()
259                        - bounds.left()
260                        - bounds.size.width.half()
261                }
262            }
263            _ => {
264                // Ref: https://github.com/zed-industries/zed/blob/0d145289e0867a8d5d63e5e1397a5ca69c9d49c3/crates/gpui/src/elements/div.rs#L3026
265                if self.axis.is_vertical() {
266                    if bounds.top() + scroll_offset.y < content_bounds.top() {
267                        scroll_offset.y = content_bounds.top() - bounds.top()
268                    } else if bounds.bottom() + scroll_offset.y > content_bounds.bottom() {
269                        scroll_offset.y = content_bounds.bottom() - bounds.bottom();
270                    }
271                } else {
272                    if bounds.left() + scroll_offset.x < content_bounds.left() {
273                        scroll_offset.x = content_bounds.left() - bounds.left();
274                    } else if bounds.right() + scroll_offset.x > content_bounds.right() {
275                        scroll_offset.x = content_bounds.right() - bounds.right();
276                    }
277                }
278            }
279        }
280        self.scroll_handle.set_offset(scroll_offset);
281        scroll_offset
282    }
283}
284
285/// Frame state used by the [VirtualItem].
286pub struct VirtualListFrameState {
287    /// Visible items to be painted.
288    items: SmallVec<[AnyElement; 32]>,
289    size_layout: ItemSizeLayout,
290}
291
292#[derive(Default, Clone)]
293pub struct ItemSizeLayout {
294    items_sizes: Rc<Vec<Size<Pixels>>>,
295    content_size: Size<Pixels>,
296    sizes: Vec<Pixels>,
297    origins: Vec<Pixels>,
298    last_layout_bounds: Bounds<Pixels>,
299}
300
301impl IntoElement for VirtualList {
302    type Element = Self;
303
304    fn into_element(self) -> Self::Element {
305        self
306    }
307}
308
309impl Element for VirtualList {
310    type RequestLayoutState = VirtualListFrameState;
311    type PrepaintState = Option<Hitbox>;
312
313    fn id(&self) -> Option<ElementId> {
314        Some(self.id.clone())
315    }
316
317    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
318        None
319    }
320
321    fn request_layout(
322        &mut self,
323        global_id: Option<&GlobalElementId>,
324        inspector_id: Option<&gpui::InspectorElementId>,
325        window: &mut Window,
326        cx: &mut App,
327    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
328        let rem_size = window.rem_size();
329        let font_size = window.text_style().font_size.to_pixels(rem_size);
330        let mut size_layout = ItemSizeLayout::default();
331
332        let layout_id = self.base.interactivity().request_layout(
333            global_id,
334            inspector_id,
335            window,
336            cx,
337            |style, window, cx| {
338                size_layout = window.with_element_state(
339                    global_id.unwrap(),
340                    |state: Option<ItemSizeLayout>, _window| {
341                        let mut state = state.unwrap_or(ItemSizeLayout::default());
342
343                        // Including the gap between items for calculate the item size
344                        let gap = style
345                            .gap
346                            .along(self.axis)
347                            .to_pixels(font_size.into(), rem_size);
348
349                        if state.items_sizes != self.item_sizes {
350                            state.items_sizes = self.item_sizes.clone();
351                            // Prepare each item's size by axis
352                            state.sizes = self
353                                .item_sizes
354                                .iter()
355                                .enumerate()
356                                .map(|(i, size)| {
357                                    let size = size.along(self.axis);
358                                    if i + 1 == self.items_count {
359                                        size
360                                    } else {
361                                        size + gap
362                                    }
363                                })
364                                .collect::<Vec<_>>();
365
366                            // Prepare each item's origin by axis
367                            state.origins = state
368                                .sizes
369                                .iter()
370                                .scan(px(0.), |cumulative, size| match self.axis {
371                                    Axis::Horizontal => {
372                                        let x = *cumulative;
373                                        *cumulative += *size;
374                                        Some(x)
375                                    }
376                                    Axis::Vertical => {
377                                        let y = *cumulative;
378                                        *cumulative += *size;
379                                        Some(y)
380                                    }
381                                })
382                                .collect::<Vec<_>>();
383
384                            state.content_size = if self.axis.is_horizontal() {
385                                Size {
386                                    width: px(state
387                                        .sizes
388                                        .iter()
389                                        .map(|size| size.as_f32())
390                                        .sum::<f32>()),
391                                    height: state
392                                        .items_sizes
393                                        .get(0)
394                                        .map_or(px(0.), |size| size.height),
395                                }
396                            } else {
397                                Size {
398                                    width: state
399                                        .items_sizes
400                                        .get(0)
401                                        .map_or(px(0.), |size| size.width),
402                                    height: px(state
403                                        .sizes
404                                        .iter()
405                                        .map(|size| size.as_f32())
406                                        .sum::<f32>()),
407                                }
408                            };
409                        }
410
411                        (state.clone(), state)
412                    },
413                );
414
415                let axis = self.axis;
416                let layout_id =
417                    match self.sizing_behavior {
418                        ListSizingBehavior::Infer => {
419                            window.with_text_style(style.text_style().cloned(), |window| {
420                                let size_layout = size_layout.clone();
421
422                                window.request_measured_layout(style, {
423                                    move |known_dimensions, available_space, _, _| {
424                                        let mut size = Size::default();
425                                        if axis.is_horizontal() {
426                                            size.width = known_dimensions.width.unwrap_or(
427                                                match available_space.width {
428                                                    AvailableSpace::Definite(x) => x,
429                                                    AvailableSpace::MinContent
430                                                    | AvailableSpace::MaxContent => {
431                                                        size_layout.content_size.width
432                                                    }
433                                                },
434                                            );
435                                            size.height = known_dimensions.width.unwrap_or(
436                                                match available_space.height {
437                                                    AvailableSpace::Definite(x) => x,
438                                                    AvailableSpace::MinContent
439                                                    | AvailableSpace::MaxContent => {
440                                                        size_layout.content_size.height
441                                                    }
442                                                },
443                                            );
444                                        } else {
445                                            size.width = known_dimensions.width.unwrap_or(
446                                                match available_space.width {
447                                                    AvailableSpace::Definite(x) => x,
448                                                    AvailableSpace::MinContent
449                                                    | AvailableSpace::MaxContent => {
450                                                        size_layout.content_size.width
451                                                    }
452                                                },
453                                            );
454                                            size.height = known_dimensions.height.unwrap_or(
455                                                match available_space.height {
456                                                    AvailableSpace::Definite(x) => x,
457                                                    AvailableSpace::MinContent
458                                                    | AvailableSpace::MaxContent => {
459                                                        size_layout.content_size.height
460                                                    }
461                                                },
462                                            );
463                                        }
464
465                                        size
466                                    }
467                                })
468                            })
469                        }
470                        ListSizingBehavior::Auto => window
471                            .with_text_style(style.text_style().cloned(), |window| {
472                                window.request_layout(style, None, cx)
473                            }),
474                    };
475
476                layout_id
477            },
478        );
479
480        (
481            layout_id,
482            VirtualListFrameState {
483                items: SmallVec::new(),
484                size_layout,
485            },
486        )
487    }
488
489    fn prepaint(
490        &mut self,
491        global_id: Option<&GlobalElementId>,
492        inspector_id: Option<&gpui::InspectorElementId>,
493        bounds: Bounds<Pixels>,
494        layout: &mut Self::RequestLayoutState,
495        window: &mut Window,
496        cx: &mut App,
497    ) -> Self::PrepaintState {
498        layout.size_layout.last_layout_bounds = bounds;
499
500        let style = self
501            .base
502            .interactivity()
503            .compute_style(global_id, None, window, cx);
504        let border_widths = style.border_widths.to_pixels(window.rem_size());
505        let paddings = style
506            .padding
507            .to_pixels(bounds.size.into(), window.rem_size());
508
509        let item_sizes = &layout.size_layout.sizes;
510        let item_origins = &layout.size_layout.origins;
511
512        let content_bounds = Bounds::from_corners(
513            bounds.origin
514                + point(
515                    border_widths.left + paddings.left,
516                    border_widths.top + paddings.top,
517                ),
518            bounds.bottom_right()
519                - point(
520                    border_widths.right + paddings.right,
521                    border_widths.bottom + paddings.bottom,
522                ),
523        );
524
525        // Update scroll_handle with the item bounds
526        let items_bounds = item_origins
527            .iter()
528            .enumerate()
529            .map(|(i, &origin)| {
530                let item_size = item_sizes[i];
531
532                Bounds {
533                    origin: match self.axis {
534                        Axis::Horizontal => point(content_bounds.left() + origin, px(0.)),
535                        Axis::Vertical => point(px(0.), content_bounds.top() + origin),
536                    },
537                    size: match self.axis {
538                        Axis::Horizontal => size(item_size, content_bounds.size.height),
539                        Axis::Vertical => size(content_bounds.size.width, item_size),
540                    },
541                }
542            })
543            .collect::<Vec<_>>();
544
545        let axis = self.axis;
546
547        let mut scroll_state = self.scroll_handle.state.borrow_mut();
548        scroll_state.axis = axis;
549        scroll_state.items_count = self.items_count;
550
551        let mut scroll_offset = self.scroll_handle.offset();
552        if let Some(scroll_to_item) = scroll_state.deferred_scroll_to_item.take() {
553            scroll_offset = self.scroll_to_deferred_item(
554                scroll_offset,
555                &items_bounds,
556                &content_bounds,
557                scroll_to_item,
558            );
559        }
560        scroll_offset = scroll_offset.min(&point(px(0.), px(0.)));
561
562        self.base.interactivity().prepaint(
563            global_id,
564            inspector_id,
565            bounds,
566            layout.size_layout.content_size,
567            window,
568            cx,
569            |_style, _, hitbox, window, cx| {
570                if self.items_count > 0 {
571                    let min_scroll_offset = content_bounds.size.along(self.axis)
572                        - layout.size_layout.content_size.along(self.axis);
573
574                    // Do not trigger scrolling if the content is smaller than the container.
575                    if min_scroll_offset.as_f32() >= 0. {
576                        scroll_offset.x = px(0.);
577                        scroll_offset.y = px(0.);
578                    }
579
580                    let is_scrolled = !scroll_offset.along(self.axis).is_zero();
581                    if is_scrolled {
582                        match self.axis {
583                            Axis::Horizontal if scroll_offset.x < min_scroll_offset => {
584                                scroll_offset.x = min_scroll_offset;
585                            }
586                            Axis::Vertical if scroll_offset.y < min_scroll_offset => {
587                                scroll_offset.y = min_scroll_offset;
588                            }
589                            _ => {}
590                        }
591                    }
592
593                    let (first_visible_element_ix, last_visible_element_ix) = match self.axis {
594                        Axis::Horizontal => {
595                            let mut cumulative_size = px(0.);
596                            let mut first_visible_element_ix = 0;
597                            for (i, &size) in item_sizes.iter().enumerate() {
598                                cumulative_size += size;
599                                if cumulative_size > -(scroll_offset.x + paddings.left) {
600                                    first_visible_element_ix = i;
601                                    break;
602                                }
603                            }
604
605                            cumulative_size = px(0.);
606                            let mut last_visible_element_ix = 0;
607                            for (i, &size) in item_sizes.iter().enumerate() {
608                                cumulative_size += size;
609                                if cumulative_size > (-scroll_offset.x + content_bounds.size.width)
610                                {
611                                    last_visible_element_ix = i + 1;
612                                    break;
613                                }
614                            }
615                            if last_visible_element_ix == 0 {
616                                last_visible_element_ix = self.items_count;
617                            } else {
618                                last_visible_element_ix += 1;
619                            }
620                            (first_visible_element_ix, last_visible_element_ix)
621                        }
622                        Axis::Vertical => {
623                            let mut cumulative_size = px(0.);
624                            let mut first_visible_element_ix = 0;
625                            for (i, &size) in item_sizes.iter().enumerate() {
626                                cumulative_size += size;
627                                if cumulative_size > -(scroll_offset.y + paddings.top) {
628                                    first_visible_element_ix = i;
629                                    break;
630                                }
631                            }
632
633                            cumulative_size = px(0.);
634                            let mut last_visible_element_ix = 0;
635                            for (i, &size) in item_sizes.iter().enumerate() {
636                                cumulative_size += size;
637                                if cumulative_size > (-scroll_offset.y + content_bounds.size.height)
638                                {
639                                    last_visible_element_ix = i + 1;
640                                    break;
641                                }
642                            }
643                            if last_visible_element_ix == 0 {
644                                last_visible_element_ix = self.items_count;
645                            } else {
646                                last_visible_element_ix += 1;
647                            }
648                            (first_visible_element_ix, last_visible_element_ix)
649                        }
650                    };
651
652                    let visible_range = first_visible_element_ix
653                        ..cmp::min(last_visible_element_ix, self.items_count);
654
655                    let items = (self.render_items)(visible_range.clone(), window, cx);
656
657                    let content_mask = ContentMask { bounds };
658                    window.with_content_mask(Some(content_mask), |window| {
659                        for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
660                            let item_origin = match self.axis {
661                                Axis::Horizontal => {
662                                    content_bounds.origin
663                                        + point(item_origins[ix] + scroll_offset.x, scroll_offset.y)
664                                }
665                                Axis::Vertical => {
666                                    content_bounds.origin
667                                        + point(scroll_offset.x, item_origins[ix] + scroll_offset.y)
668                                }
669                            };
670
671                            let available_space = match self.axis {
672                                Axis::Horizontal => size(
673                                    AvailableSpace::Definite(item_sizes[ix]),
674                                    AvailableSpace::Definite(content_bounds.size.height),
675                                ),
676                                Axis::Vertical => size(
677                                    AvailableSpace::Definite(content_bounds.size.width),
678                                    AvailableSpace::Definite(item_sizes[ix]),
679                                ),
680                            };
681
682                            item.layout_as_root(available_space, window, cx);
683                            item.prepaint_at(item_origin, window, cx);
684                            layout.items.push(item);
685                        }
686                    });
687                }
688
689                hitbox
690            },
691        )
692    }
693
694    fn paint(
695        &mut self,
696        global_id: Option<&GlobalElementId>,
697        inspector_id: Option<&gpui::InspectorElementId>,
698        bounds: Bounds<Pixels>,
699        layout: &mut Self::RequestLayoutState,
700        hitbox: &mut Self::PrepaintState,
701        window: &mut Window,
702        cx: &mut App,
703    ) {
704        self.base.interactivity().paint(
705            global_id,
706            inspector_id,
707            bounds,
708            hitbox.as_ref(),
709            window,
710            cx,
711            |_, window, cx| {
712                for item in &mut layout.items {
713                    item.paint(window, cx);
714                }
715            },
716        )
717    }
718}