ui/list.rs
1//! Virtualized list — a thin binding over gpui's `uniform_list`, and a bridge
2//! that lets [`crate::scroll::scrollbar`] report on one.
3//!
4//! Thin on purpose: gpui already does the hard part, and a wrapper that only
5//! re-exported it with extra steps would be worse than none. This module exists
6//! for two things it can guarantee that a caller otherwise has to know.
7//!
8//! **The row height.** `uniform_list` measures the *first* row it renders and
9//! lays every other one out at that height. Hand it rows of different heights
10//! and nothing errors — the content simply overlaps, at a size nobody chose.
11//! [`virtual_list`] takes the height and applies it to every row it hands back,
12//! so that cannot happen by accident.
13//!
14//! **The scroll handle.** A `UniformListScrollHandle` wraps a real
15//! [`ScrollHandle`] and gpui registers it as the list's tracked handle, so the
16//! bar's geometry is all there — behind `handle.0.borrow().base_handle`, which
17//! is not something a consumer should have to find by reading gpui's source.
18//! [`scroll_handle`] is that reach, named.
19//!
20//! ## Why not gpui's `list()`
21//!
22//! gpui has a second virtualizer for rows of *varying* height, and it cannot
23//! carry a proportional scrollbar: `ListState` speaks in `ListOffset { item_ix,
24//! offset_in_item }` — logical position, not pixels — with no maximum offset
25//! and no viewport. A thumb's length is the visible share of a total height, and
26//! a variable-height list cannot know its total without measuring every row,
27//! which is the work virtualization exists to skip. A list of thousands of rows
28//! wants a bar; a list that needs varying heights is a different component, and
29//! nothing has asked for one yet.
30//!
31//! ```ignore
32//! div().relative().h(px(240.0))
33//! .child(virtual_list("rows", rows.len(), px(28.0), &self.rows_scroll, {
34//! let rows = rows.clone();
35//! move |range, _, _| range.map(|ix| row(&rows[ix])).collect()
36//! }))
37//! .child(scroll::scrollbar("rows-bar", &list::scroll_handle(&self.rows_scroll), &self.rows_bar))
38//! ```
39
40use std::ops::Range;
41
42use gpui::{
43 App, ElementId, IntoElement, Pixels, ScrollHandle, UniformList, UniformListScrollHandle,
44 Window, prelude::*, uniform_list,
45};
46
47/// The pixel-space scroll handle inside a `UniformListScrollHandle`.
48///
49/// `uniform_list` tracks its scrolling through this one, so it carries the
50/// offset, the maximum offset and the viewport that [`crate::scroll::thumb`]
51/// needs — a virtualized list takes the same bar as any other scroller, with no
52/// second implementation behind a trait.
53///
54/// The clone shares state rather than copying it: the returned handle *is* the
55/// list's, and moving one moves the other.
56pub fn scroll_handle(handle: &UniformListScrollHandle) -> ScrollHandle {
57 handle.0.borrow().base_handle.clone()
58}
59
60/// A list that builds only the rows on screen.
61///
62/// `render` is handed the visible range and returns one element per index in
63/// it; each comes back sized to `row_height`, which is what keeps the list
64/// uniform and therefore virtualizable at all.
65///
66/// Fills its parent, which is the other thing that has to be true for any of
67/// this to work: a list with no height of its own collapses, and a collapsed
68/// list builds a single row to measure and then nothing — an empty box, no
69/// error, no clue. A virtualized list is bounded by definition, so filling is
70/// the only sane default; a caller wanting otherwise sets its own size after,
71/// and the later call wins.
72pub fn virtual_list<R>(
73 id: impl Into<ElementId>,
74 count: usize,
75 row_height: Pixels,
76 handle: &UniformListScrollHandle,
77 render: impl 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<R>,
78) -> UniformList
79where
80 R: IntoElement,
81{
82 uniform_list(id, count, move |range, window, cx| {
83 render(range, window, cx)
84 .into_iter()
85 .map(|row| gpui::div().h(row_height).w_full().child(row))
86 .collect::<Vec<_>>()
87 })
88 .size_full()
89 .track_scroll(handle)
90}