Skip to main content

gpui/elements/
uniform_list.rs

1//! Lists of rows that all have the same height, rendered lazily.
2//!
3//! [`uniform_list`] renders only the rows in the viewport. It measures one row, then
4//! places every row at `index * height`, so it never runs layout on rows that are
5//! off screen. Use [`crate::list`] when rows have different heights.
6//!
7//! The [lists and scrolling guide](crate::_documentation::guides::lists_and_scrolling) compares
8//! both list elements and shows how to keep their state on a view.
9
10use crate::{
11    AnyElement, App, AvailableSpace, Bounds, Element, ElementId, Entity, GlobalElementId, Hitbox,
12    InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
13    ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, StyleRefinement, Styled,
14    Window, point, px, size,
15};
16use smallvec::SmallVec;
17use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
18
19use super::ListHorizontalSizingBehavior;
20
21/// Creates a list of rows that all have the same height, and renders only the rows in view.
22///
23/// Use this for long lists, such as tables, file trees or contact lists, when every row has
24/// the same height. Use [`list`](fn@crate::list) when rows vary in height.
25///
26/// `f` receives the range of visible row indices and returns one element per row, in order.
27/// Each frame it runs for the visible range. It also runs for a single row to measure it, in
28/// both layout and prepaint.
29/// So `f` must accept any range within `0..item_count`, and every row it returns must be the
30/// height of the measured row.
31///
32/// The list sets its own overflow to scroll. Give it a bounded height, from its parent or from
33/// [`Styled::h`] or [`Styled::size_full`]. By default the height comes from the style. With
34/// [`UniformList::with_sizing_behavior`] set to [`ListSizingBehavior::Infer`], a list with no
35/// bounded height is as tall as all its rows, so every row is in view and gets rendered.
36///
37/// Rows outside the viewport are not created. Keep your data on your view and index into it
38/// from `f`, because row elements are dropped as they scroll away.
39///
40/// # Examples
41///
42/// ```
43/// # use gpui::*;
44/// # use std::{cell::Cell, rc::Rc};
45/// struct Contacts {
46///     names: Rc<Vec<String>>,
47///     widest_batch: Rc<Cell<usize>>,
48/// }
49///
50/// impl Render for Contacts {
51///     fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
52///         let names = self.names.clone();
53///         let widest_batch = self.widest_batch.clone();
54///         uniform_list("contacts", names.len(), move |range, _window, _cx| {
55///             widest_batch.set(widest_batch.get().max(range.len()));
56///             range
57///                 .map(|ix| div().id(ix).h(px(32.)).child(names[ix].clone()))
58///                 .collect::<Vec<_>>()
59///         })
60///         .size_full()
61///     }
62/// }
63/// # let mut app = TestApp::new();
64/// # let widest_batch = Rc::new(Cell::new(0));
65/// # let names: Rc<Vec<String>> = Rc::new((0..10_000).map(|ix| format!("Contact {ix}")).collect());
66/// # let mut window = app.open_window(|_, _| Contacts { names, widest_batch: widest_batch.clone() });
67/// # window.draw();
68/// // Only the rows on screen are passed to the closure, never all 10,000.
69/// # assert!(widest_batch.get() > 0 && widest_batch.get() < 1_000);
70/// ```
71#[track_caller]
72pub fn uniform_list<R>(
73    id: impl Into<ElementId>,
74    item_count: usize,
75    f: impl 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<R>,
76) -> UniformList
77where
78    R: IntoElement,
79{
80    let id = id.into();
81    let mut base_style = StyleRefinement::default();
82    base_style.overflow.y = Some(Overflow::Scroll);
83
84    let render_range = move |range: Range<usize>, window: &mut Window, cx: &mut App| {
85        f(range, window, cx)
86            .into_iter()
87            .map(|component| component.into_any_element())
88            .collect()
89    };
90
91    UniformList {
92        item_count,
93        item_to_measure_index: 0,
94        render_items: Box::new(render_range),
95        decorations: Vec::new(),
96        interactivity: Interactivity {
97            element_id: Some(id),
98            base_style: Box::new(base_style),
99            ..Interactivity::new()
100        },
101        scroll_handle: None,
102        sizing_behavior: ListSizingBehavior::default(),
103        horizontal_sizing_behavior: ListHorizontalSizingBehavior::default(),
104    }
105}
106
107/// The element returned by [`uniform_list`]. Configure it with the builder methods, then
108/// return it from `render`. To read or set the scroll position, pass a
109/// [`UniformListScrollHandle`] to [`UniformList::track_scroll`].
110pub struct UniformList {
111    item_count: usize,
112    item_to_measure_index: usize,
113    render_items: Box<
114        dyn for<'a> Fn(Range<usize>, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>,
115    >,
116    decorations: Vec<Box<dyn UniformListDecoration>>,
117    interactivity: Interactivity,
118    scroll_handle: Option<UniformListScrollHandle>,
119    sizing_behavior: ListSizingBehavior,
120    horizontal_sizing_behavior: ListHorizontalSizingBehavior,
121}
122
123/// The rows and decorations that a [`UniformList`] lays out for one frame. You don't construct it.
124pub struct UniformListFrameState {
125    items: SmallVec<[AnyElement; 32]>,
126    decorations: SmallVec<[AnyElement; 2]>,
127}
128
129/// A handle that reads and sets the scroll position of a [`UniformList`].
130///
131/// Store it on your view, and pass it to [`UniformList::track_scroll`] when you build the list
132/// each frame. Clones share the same state.
133///
134/// Scroll requests (`scroll_to_*`) take effect during the next prepaint, not immediately. A
135/// request does nothing until a list tracks the handle.
136#[derive(Clone, Debug, Default)]
137pub struct UniformListScrollHandle(pub Rc<RefCell<UniformListScrollState>>);
138
139/// Where a scroll request places the target row in the viewport.
140///
141/// The list stops at its first and last row, so a row near either end can't always reach the
142/// position you ask for. It lands at the closest position the list allows.
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub enum ScrollStrategy {
145    /// Places the row at the top of the viewport.
146    Top,
147    /// Places the row in the middle of the viewport, as close as the start and end of the list allow.
148    Center,
149    /// Places the row at the bottom of the viewport, as close as the end of the list allows.
150    Bottom,
151    /// Moves the least amount that shows the row. A row above the viewport is placed at the top,
152    /// and a row below it at the bottom. A row that is already fully visible doesn't move the list.
153    Nearest,
154}
155
156#[derive(Clone, Copy, Debug)]
157#[allow(missing_docs)]
158/// A scroll request that waits for the next prepaint.
159pub struct DeferredScrollToItem {
160    /// The row to scroll to.
161    pub item_index: usize,
162    /// Where the row should land.
163    pub strategy: ScrollStrategy,
164    /// The number of rows to keep clear, as described for
165    /// [`UniformListScrollHandle::scroll_to_item_with_offset`].
166    pub offset: usize,
167    /// Whether to apply `strategy` even when the row is already visible.
168    pub scroll_strict: bool,
169}
170
171#[derive(Clone, Debug, Default)]
172#[allow(missing_docs)]
173/// The state that a [`UniformListScrollHandle`] shares with its [`UniformList`].
174pub struct UniformListScrollState {
175    /// The scroll position of the list. [`UniformList::track_scroll`] shares it with the list.
176    pub base_handle: ScrollHandle,
177    /// A scroll request from a `scroll_to_*` method, applied and cleared on the next prepaint.
178    pub deferred_scroll_to_item: Option<DeferredScrollToItem>,
179    /// The viewport and content sizes from the last prepaint, or `None` before the first one.
180    pub last_item_size: Option<ItemSize>,
181    /// Whether item 0 is at the bottom of the list, as set by [`UniformList::y_flipped`].
182    pub y_flipped: bool,
183}
184
185#[derive(Copy, Clone, Debug, Default)]
186/// The sizes of a [`UniformList`]'s viewport and of all its rows, from the last prepaint.
187pub struct ItemSize {
188    /// The size of the viewport, after the list's border and padding.
189    pub item: Size<Pixels>,
190    /// The size of all the rows together: the row height times the row count, and the
191    /// width of the content, which is wider than the viewport when rows overflow horizontally.
192    pub contents: Size<Pixels>,
193}
194
195impl UniformListScrollHandle {
196    fn revised(&self) -> std::cell::RefMut<'_, UniformListScrollState> {
197        let state = self.0.borrow_mut();
198        state.base_handle.revision().bump();
199        state
200    }
201
202    /// Creates a handle that isn't attached to a list yet.
203    ///
204    /// Pass it to [`UniformList::track_scroll`] when you build the list.
205    pub fn new() -> Self {
206        Self(Rc::new(RefCell::new(UniformListScrollState {
207            base_handle: ScrollHandle::new(),
208            deferred_scroll_to_item: None,
209            last_item_size: None,
210            y_flipped: false,
211        })))
212    }
213
214    /// Scrolls the least amount that brings row `ix` fully into view, placed by `strategy`.
215    ///
216    /// This is non-strict: a row that is already fully visible doesn't move the list, so
217    /// `ScrollStrategy::Top` does nothing for a visible row. Use [`Self::scroll_to_item_strict`]
218    /// to place the row every time. The scroll takes effect on the next prepaint.
219    ///
220    /// # Examples
221    ///
222    /// ```
223    /// # use gpui::*;
224    /// struct Rows {
225    ///     scroll: UniformListScrollHandle,
226    /// }
227    ///
228    /// impl Render for Rows {
229    ///     fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
230    ///         div().h(px(320.)).child(
231    ///             uniform_list("rows", 100, |range, _window, _cx| {
232    ///                 range
233    ///                     .map(|ix| div().id(ix).h(px(32.)).child(format!("Row {ix}")))
234    ///                     .collect::<Vec<_>>()
235    ///             })
236    ///             .track_scroll(&self.scroll)
237    ///             .size_full(),
238    ///         )
239    ///     }
240    /// }
241    /// # let mut app = TestApp::new();
242    /// # let mut window = app.open_window(|_, _| Rows { scroll: UniformListScrollHandle::new() });
243    /// window.update(|rows, _, _| rows.scroll.scroll_to_item(3, ScrollStrategy::Top));
244    /// window.draw();
245    /// // Row 3 is already in the 320px viewport, so the list didn't move.
246    /// window.read(|rows, _| assert_eq!(rows.scroll.0.borrow().base_handle.offset().y, px(0.)));
247    ///
248    /// window.update(|rows, _, _| rows.scroll.scroll_to_item_strict(3, ScrollStrategy::Top));
249    /// window.draw();
250    /// // The strict form puts row 3 at the top: 3 rows of 32px scrolled past.
251    /// window.read(|rows, _| assert_eq!(rows.scroll.0.borrow().base_handle.offset().y, px(-96.)));
252    /// ```
253    pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
254        self.revised().deferred_scroll_to_item = Some(DeferredScrollToItem {
255            item_index: ix,
256            strategy,
257            offset: 0,
258            scroll_strict: false,
259        });
260    }
261
262    /// Places row `ix` where `strategy` says, even when the row is already visible.
263    ///
264    /// Unlike [`Self::scroll_to_item`], this forces the requested placement when the strategy
265    /// supports one. With [`ScrollStrategy::Nearest`], a visible row still doesn't move. Use it
266    /// when a row must land at a fixed position, such as the top after a keyboard jump. The scroll
267    /// takes effect on the next prepaint; see [`Self::scroll_to_item`] for an example.
268    pub fn scroll_to_item_strict(&self, ix: usize, strategy: ScrollStrategy) {
269        self.revised().deferred_scroll_to_item = Some(DeferredScrollToItem {
270            item_index: ix,
271            strategy,
272            offset: 0,
273            scroll_strict: true,
274        });
275    }
276
277    /// Scrolls like [`Self::scroll_to_item`], and keeps `offset` rows clear at the top of the viewport.
278    ///
279    /// `offset` shrinks the viewport by that many rows from the top, and `strategy` places the
280    /// row in what remains:
281    /// - [`ScrollStrategy::Top`] puts the row just below the cleared rows.
282    /// - [`ScrollStrategy::Center`] centers the row in the rows below the cleared rows.
283    /// - [`ScrollStrategy::Bottom`] places the row at the bottom of the viewport. `offset` affects
284    ///   only whether the row counts as already visible, not where it lands.
285    ///
286    /// This is non-strict, so a row that is visible below the cleared rows doesn't move the list.
287    pub fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) {
288        self.revised().deferred_scroll_to_item = Some(DeferredScrollToItem {
289            item_index: ix,
290            strategy,
291            offset,
292            scroll_strict: false,
293        });
294    }
295
296    /// Places row `ix` like [`Self::scroll_to_item_with_offset`], even when the row is already visible.
297    ///
298    /// The `offset` rules are the same as for [`Self::scroll_to_item_with_offset`].
299    pub fn scroll_to_item_strict_with_offset(
300        &self,
301        ix: usize,
302        strategy: ScrollStrategy,
303        offset: usize,
304    ) {
305        self.revised().deferred_scroll_to_item = Some(DeferredScrollToItem {
306            item_index: ix,
307            strategy,
308            offset,
309            scroll_strict: true,
310        });
311    }
312
313    /// Returns whether item 0 is at the bottom of the list, as set by [`UniformList::y_flipped`]
314    /// on the last frame.
315    pub fn y_flipped(&self) -> bool {
316        self.0.borrow().y_flipped
317    }
318
319    /// Returns the index of the first visible row.
320    ///
321    /// If a scroll request is pending, this returns the row that request targets instead, so the
322    /// value is right after a request and one frame later. It is zero before the first prepaint.
323    ///
324    /// Only available with the `test-support` feature, or in GPUI's own tests.
325    #[cfg(any(test, feature = "test-support"))]
326    pub fn logical_scroll_top_index(&self) -> usize {
327        let this = self.0.borrow();
328        this.deferred_scroll_to_item
329            .as_ref()
330            .map(|deferred| deferred.item_index)
331            .unwrap_or_else(|| this.base_handle.logical_scroll_top().0)
332    }
333
334    /// Returns whether the rows are taller than the viewport, as of the last prepaint.
335    ///
336    /// Returns `false` before the first prepaint.
337    pub fn is_scrollable(&self) -> bool {
338        if let Some(size) = self.0.borrow().last_item_size {
339            size.contents.height > size.item.height
340        } else {
341            false
342        }
343    }
344
345    /// Returns whether the list shows its last row, or `None` if the list isn't scrollable.
346    ///
347    /// `None` is also returned before the first prepaint.
348    pub fn is_scrolled_to_end(&self) -> Option<bool> {
349        let state = self.0.borrow();
350        let max_offset = state.base_handle.max_offset();
351        if max_offset.y <= px(0.) {
352            return None;
353        }
354        let offset = state.base_handle.offset();
355        Some(-offset.y >= max_offset.y)
356    }
357
358    /// Scrolls to the last row, as a request for the next prepaint.
359    pub fn scroll_to_bottom(&self) {
360        self.scroll_to_item(usize::MAX, ScrollStrategy::Bottom);
361    }
362}
363
364impl Styled for UniformList {
365    fn style(&mut self) -> &mut StyleRefinement {
366        &mut self.interactivity.base_style
367    }
368}
369
370impl Element for UniformList {
371    type RequestLayoutState = UniformListFrameState;
372    type PrepaintState = Option<Hitbox>;
373
374    fn id(&self) -> Option<ElementId> {
375        self.interactivity.element_id.clone()
376    }
377
378    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
379        None
380    }
381
382    fn request_layout(
383        &mut self,
384        global_id: Option<&GlobalElementId>,
385        inspector_id: Option<&InspectorElementId>,
386        window: &mut Window,
387        cx: &mut App,
388    ) -> (LayoutId, Self::RequestLayoutState) {
389        let max_items = self.item_count;
390        let item_size = self.measure_item(None, window, cx);
391        let layout_id = self.interactivity.request_layout(
392            global_id,
393            inspector_id,
394            window,
395            cx,
396            |style, window, cx| match self.sizing_behavior {
397                ListSizingBehavior::Infer => {
398                    window.with_text_style(style.text_style().cloned(), |window| {
399                        window.request_measured_layout(
400                            style,
401                            move |known_dimensions, available_space, _window, _cx| {
402                                let desired_height = item_size.height * max_items;
403                                let width = known_dimensions.width.unwrap_or(match available_space
404                                    .width
405                                {
406                                    AvailableSpace::Definite(x) => x,
407                                    AvailableSpace::MinContent | AvailableSpace::MaxContent => {
408                                        item_size.width
409                                    }
410                                });
411                                let height = match available_space.height {
412                                    AvailableSpace::Definite(height) => desired_height.min(height),
413                                    AvailableSpace::MinContent | AvailableSpace::MaxContent => {
414                                        desired_height
415                                    }
416                                };
417                                size(width, height)
418                            },
419                        )
420                    })
421                }
422                ListSizingBehavior::Auto => window
423                    .with_text_style(style.text_style().cloned(), |window| {
424                        window.request_layout(style, None, cx)
425                    }),
426            },
427        );
428
429        (
430            layout_id,
431            UniformListFrameState {
432                items: SmallVec::new(),
433                decorations: SmallVec::new(),
434            },
435        )
436    }
437
438    fn prepaint(
439        &mut self,
440        global_id: Option<&GlobalElementId>,
441        inspector_id: Option<&InspectorElementId>,
442        bounds: Bounds<Pixels>,
443        frame_state: &mut Self::RequestLayoutState,
444        window: &mut Window,
445        cx: &mut App,
446    ) -> Option<Hitbox> {
447        let style = self
448            .interactivity
449            .compute_style(global_id, None, window, cx);
450        let border = style.border_widths.to_pixels(window.rem_size());
451        let padding = style
452            .padding
453            .to_pixels(bounds.size.into(), window.rem_size());
454
455        let padded_bounds = Bounds::from_corners(
456            bounds.origin + point(border.left + padding.left, border.top + padding.top),
457            bounds.bottom_right()
458                - point(border.right + padding.right, border.bottom + padding.bottom),
459        );
460
461        let can_scroll_horizontally = matches!(
462            self.horizontal_sizing_behavior,
463            ListHorizontalSizingBehavior::Unconstrained
464        );
465
466        let longest_item_size = self.measure_item(None, window, cx);
467        let content_width = if can_scroll_horizontally {
468            padded_bounds.size.width.max(longest_item_size.width)
469        } else {
470            padded_bounds.size.width
471        };
472        let content_size = Size {
473            width: content_width,
474            height: longest_item_size.height * self.item_count,
475        };
476
477        let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
478        let item_height = longest_item_size.height;
479        let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| {
480            let mut handle = handle.0.borrow_mut();
481            handle.last_item_size = Some(ItemSize {
482                item: padded_bounds.size,
483                contents: content_size,
484            });
485            handle.deferred_scroll_to_item.take()
486        });
487
488        self.interactivity.prepaint(
489            global_id,
490            inspector_id,
491            bounds,
492            content_size,
493            window,
494            cx,
495            |_style, mut scroll_offset, hitbox, window, cx| {
496                let y_flipped = if let Some(scroll_handle) = &self.scroll_handle {
497                    let scroll_state = scroll_handle.0.borrow();
498                    scroll_state.y_flipped
499                } else {
500                    false
501                };
502
503                if self.item_count > 0 {
504                    let content_height = item_height * self.item_count;
505
506                    let is_scrolled_vertically = !scroll_offset.y.is_zero();
507                    let max_scroll_offset = padded_bounds.size.height - content_height;
508
509                    if is_scrolled_vertically && scroll_offset.y < max_scroll_offset {
510                        shared_scroll_offset.borrow_mut().y = max_scroll_offset;
511                        scroll_offset.y = max_scroll_offset;
512                    }
513
514                    let content_width = content_size.width + padding.left + padding.right;
515                    let is_scrolled_horizontally =
516                        can_scroll_horizontally && !scroll_offset.x.is_zero();
517                    if is_scrolled_horizontally && content_width <= padded_bounds.size.width {
518                        shared_scroll_offset.borrow_mut().x = Pixels::ZERO;
519                        scroll_offset.x = Pixels::ZERO;
520                    }
521
522                    if let Some(DeferredScrollToItem {
523                        mut item_index,
524                        mut strategy,
525                        offset,
526                        scroll_strict,
527                    }) = shared_scroll_to_item
528                    {
529                        if y_flipped {
530                            item_index =
531                                self.item_count.saturating_sub(item_index.saturating_add(1));
532                        }
533                        let list_height = padded_bounds.size.height;
534                        let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
535                        let item_top = item_height * item_index;
536                        let item_bottom = item_top + item_height;
537                        let scroll_top = -updated_scroll_offset.y;
538                        let offset_pixels = item_height * offset;
539
540                        // is the selected item above/below currently visible items
541                        let is_above = item_top < scroll_top + offset_pixels;
542                        let is_below = item_bottom > scroll_top + list_height;
543
544                        if scroll_strict || is_above || is_below {
545                            if strategy == ScrollStrategy::Nearest {
546                                if is_above {
547                                    strategy = ScrollStrategy::Top;
548                                } else if is_below {
549                                    strategy = ScrollStrategy::Bottom;
550                                }
551                            }
552
553                            let max_scroll_offset =
554                                (content_height - list_height).max(Pixels::ZERO);
555                            match strategy {
556                                ScrollStrategy::Top => {
557                                    updated_scroll_offset.y = -(item_top - offset_pixels)
558                                        .clamp(Pixels::ZERO, max_scroll_offset);
559                                }
560                                ScrollStrategy::Center => {
561                                    let item_center = item_top + item_height / 2.0;
562
563                                    let viewport_height = list_height - offset_pixels;
564                                    let viewport_center = offset_pixels + viewport_height / 2.0;
565                                    let target_scroll_top = item_center - viewport_center;
566                                    updated_scroll_offset.y =
567                                        -target_scroll_top.clamp(Pixels::ZERO, max_scroll_offset);
568                                }
569                                ScrollStrategy::Bottom => {
570                                    updated_scroll_offset.y = -(item_bottom - list_height)
571                                        .clamp(Pixels::ZERO, max_scroll_offset);
572                                }
573                                ScrollStrategy::Nearest => {
574                                    // Nearest, but the item is visible -> no scroll is required
575                                }
576                            }
577                        }
578                        scroll_offset = *updated_scroll_offset
579                    }
580
581                    let scroll_top = -scroll_offset.y.to_f64();
582                    let item_height_f64 = item_height.to_f64();
583                    let first_visible_element_ix =
584                        ((scroll_top - padding.top.to_f64()) / item_height_f64).floor() as usize;
585                    let last_visible_element_ix = ((scroll_top
586                        + padded_bounds.size.height.to_f64())
587                        / item_height_f64)
588                        .ceil() as usize;
589
590                    let visible_range = first_visible_element_ix
591                        ..cmp::min(last_visible_element_ix, self.item_count);
592
593                    let items = if y_flipped {
594                        let flipped_range = self.item_count.saturating_sub(visible_range.end)
595                            ..self.item_count.saturating_sub(visible_range.start);
596                        let mut items = (self.render_items)(flipped_range, window, cx);
597                        items.reverse();
598                        items
599                    } else {
600                        (self.render_items)(visible_range.clone(), window, cx)
601                    };
602
603                    window.with_content_mask(
604                        Some(style.scroll_mask(bounds, window.rem_size())),
605                        |window| {
606                            for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
607                                let item_top = item_height_f64 * ix as f64 - scroll_top;
608                                let item_origin = padded_bounds.origin
609                                    + point(scroll_offset.x, Pixels::from(item_top));
610                                let available_width = if can_scroll_horizontally {
611                                    padded_bounds.size.width + scroll_offset.x.abs()
612                                } else {
613                                    padded_bounds.size.width
614                                };
615                                let available_space = size(
616                                    AvailableSpace::Definite(available_width),
617                                    AvailableSpace::Definite(item_height),
618                                );
619                                item.layout_as_root(available_space, window, cx);
620                                item.prepaint_at(item_origin, window, cx);
621                                frame_state.items.push(item);
622                            }
623
624                            let bounds = Bounds::new(
625                                padded_bounds.origin + scroll_offset,
626                                padded_bounds.size,
627                            );
628                            for decoration in &self.decorations {
629                                let mut decoration = decoration.as_ref().compute(
630                                    visible_range.clone(),
631                                    bounds,
632                                    scroll_offset,
633                                    item_height,
634                                    self.item_count,
635                                    window,
636                                    cx,
637                                );
638                                let available_space = size(
639                                    AvailableSpace::Definite(bounds.size.width),
640                                    AvailableSpace::Definite(bounds.size.height),
641                                );
642                                decoration.layout_as_root(available_space, window, cx);
643                                decoration.prepaint_at(bounds.origin, window, cx);
644                                frame_state.decorations.push(decoration);
645                            }
646                        },
647                    );
648                }
649
650                hitbox
651            },
652        )
653    }
654
655    fn paint(
656        &mut self,
657        global_id: Option<&GlobalElementId>,
658        inspector_id: Option<&InspectorElementId>,
659        bounds: Bounds<crate::Pixels>,
660        request_layout: &mut Self::RequestLayoutState,
661        hitbox: &mut Option<Hitbox>,
662        window: &mut Window,
663        cx: &mut App,
664    ) {
665        self.interactivity.paint(
666            global_id,
667            inspector_id,
668            bounds,
669            hitbox.as_ref(),
670            window,
671            cx,
672            |_, window, cx| {
673                for item in &mut request_layout.items {
674                    item.paint(window, cx);
675                }
676                for decoration in &mut request_layout.decorations {
677                    decoration.paint(window, cx);
678                }
679            },
680        )
681    }
682}
683
684impl IntoElement for UniformList {
685    type Element = Self;
686
687    fn into_element(self) -> Self::Element {
688        self
689    }
690}
691
692/// An element drawn over the rows of a [`UniformList`], such as indent guides.
693///
694/// Add one with [`UniformList::with_decoration`]. The list computes it each frame, after the rows.
695///
696/// # Examples
697///
698/// ```
699/// # use gpui::*;
700/// # use std::{cell::Cell, ops::Range, rc::Rc};
701/// struct CountCalls(Rc<Cell<usize>>);
702///
703/// impl UniformListDecoration for CountCalls {
704///     fn compute(
705///         &self,
706///         _visible_range: Range<usize>,
707///         _bounds: Bounds<Pixels>,
708///         _scroll_offset: Point<Pixels>,
709///         _item_height: Pixels,
710///         _item_count: usize,
711///         _window: &mut Window,
712///         _cx: &mut App,
713///     ) -> AnyElement {
714///         self.0.set(self.0.get() + 1);
715///         div().into_any_element()
716///     }
717/// }
718///
719/// struct Rows {
720///     calls: Rc<Cell<usize>>,
721///     scroll: UniformListScrollHandle,
722/// }
723///
724/// impl Render for Rows {
725///     fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
726///         uniform_list("rows", 100, |range, _window, _cx| {
727///             range
728///                 .map(|ix| div().id(ix).h(px(32.)).child(format!("Row {ix}")))
729///                 .collect::<Vec<_>>()
730///         })
731///         .track_scroll(&self.scroll)
732///         .with_decoration(CountCalls(self.calls.clone()))
733///         .size_full()
734///     }
735/// }
736/// # let mut app = TestApp::new();
737/// # let calls = Rc::new(Cell::new(0));
738/// # let mut window = app.open_window(|_, _| Rows {
739/// #     calls: calls.clone(),
740/// #     scroll: UniformListScrollHandle::new(),
741/// # });
742/// window.draw();
743/// assert!(calls.get() > 0);
744/// ```
745pub trait UniformListDecoration {
746    /// Returns the element to draw, given the rows in view and the list's geometry.
747    ///
748    /// `visible_range` holds the rows in view. `bounds` has the viewport's size, moved by
749    /// `scroll_offset`, so that row `ix` is at `bounds.origin + point(0, item_height * ix)`.
750    /// `scroll_offset` is zero or negative, and `item_count` is the number of rows. The element
751    /// is laid out at `bounds`.
752    fn compute(
753        &self,
754        visible_range: Range<usize>,
755        bounds: Bounds<Pixels>,
756        scroll_offset: Point<Pixels>,
757        item_height: Pixels,
758        item_count: usize,
759        window: &mut Window,
760        cx: &mut App,
761    ) -> AnyElement;
762}
763
764impl<T: UniformListDecoration + 'static> UniformListDecoration for Entity<T> {
765    fn compute(
766        &self,
767        visible_range: Range<usize>,
768        bounds: Bounds<Pixels>,
769        scroll_offset: Point<Pixels>,
770        item_height: Pixels,
771        item_count: usize,
772        window: &mut Window,
773        cx: &mut App,
774    ) -> AnyElement {
775        self.update(cx, |inner, cx| {
776            inner.compute(
777                visible_range,
778                bounds,
779                scroll_offset,
780                item_height,
781                item_count,
782                window,
783                cx,
784            )
785        })
786    }
787}
788
789impl UniformList {
790    /// Chooses the row that sets the width and height of every row. `None` measures row 0.
791    ///
792    /// An index past the last row measures the last row. The list measures the chosen row every
793    /// frame, so the row should be cheap to render.
794    pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
795        self.item_to_measure_index = item_index.unwrap_or(0);
796        self
797    }
798
799    /// Sets how the list sizes itself, like [`List::with_sizing_behavior`](crate::List::with_sizing_behavior).
800    ///
801    /// [`ListSizingBehavior::Auto`], the default, takes the size from the style.
802    /// [`ListSizingBehavior::Infer`] makes the list as tall as all its rows, up to the space its
803    /// parent gives it.
804    pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
805        self.sizing_behavior = behavior;
806        self
807    }
808
809    /// Sets whether rows can be wider than the list.
810    ///
811    /// With [`ListHorizontalSizingBehavior::FitList`], the default, rows are no wider than the list.
812    /// With [`ListHorizontalSizingBehavior::Unconstrained`], the content is as wide as the row
813    /// chosen by [`Self::with_width_from_item`], and the list scrolls horizontally when that is
814    /// wider than the list.
815    pub fn with_horizontal_sizing_behavior(
816        mut self,
817        behavior: ListHorizontalSizingBehavior,
818    ) -> Self {
819        self.horizontal_sizing_behavior = behavior;
820        match behavior {
821            ListHorizontalSizingBehavior::FitList => {
822                self.interactivity.base_style.overflow.x = None;
823            }
824            ListHorizontalSizingBehavior::Unconstrained => {
825                self.interactivity.base_style.overflow.x = Some(Overflow::Scroll);
826            }
827        }
828        self
829    }
830
831    /// Adds a decoration that is drawn over the rows. Decorations are drawn in the order added.
832    pub fn with_decoration(mut self, decoration: impl UniformListDecoration + 'static) -> Self {
833        self.decorations.push(Box::new(decoration));
834        self
835    }
836
837    fn measure_item(
838        &self,
839        list_width: Option<Pixels>,
840        window: &mut Window,
841        cx: &mut App,
842    ) -> Size<Pixels> {
843        if self.item_count == 0 {
844            return Size::default();
845        }
846
847        let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
848        let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx);
849        let Some(mut item_to_measure) = items.pop() else {
850            return Size::default();
851        };
852        let available_space = size(
853            list_width.map_or(AvailableSpace::MaxContent, |width| {
854                AvailableSpace::Definite(width)
855            }),
856            AvailableSpace::MinContent,
857        );
858        item_to_measure.layout_as_root(available_space, window, cx)
859    }
860
861    /// Connects the list to `handle`, so the list reads scroll requests from it and writes its
862    /// scroll position to it.
863    ///
864    /// Without this, the list still scrolls with the wheel, but nothing outside it can read or
865    /// set the position.
866    pub fn track_scroll(mut self, handle: &UniformListScrollHandle) -> Self {
867        self.interactivity.tracked_scroll_handle = Some(handle.0.borrow().base_handle.clone());
868        self.scroll_handle = Some(handle.clone());
869        self
870    }
871
872    /// Puts item 0 at the bottom of the list, so the first row sits at the bottom and the last at
873    /// the top.
874    ///
875    /// The list keeps its scroll position on the same rows when the value changes. This only takes
876    /// effect when a handle is set with [`Self::track_scroll`]. Call it after `track_scroll`,
877    /// because it reads the handle that `track_scroll` sets.
878    pub fn y_flipped(mut self, y_flipped: bool) -> Self {
879        if let Some(ref scroll_handle) = self.scroll_handle {
880            let mut scroll_state = scroll_handle.0.borrow_mut();
881            let mut base_handle = &scroll_state.base_handle;
882            let offset = base_handle.offset();
883            match scroll_state.last_item_size {
884                Some(last_size) if scroll_state.y_flipped != y_flipped => {
885                    let new_y_offset =
886                        -(offset.y + last_size.contents.height - last_size.item.height);
887                    base_handle.set_offset(point(offset.x, new_y_offset));
888                    scroll_state.y_flipped = y_flipped;
889                }
890                // Handle case where list is initially flipped.
891                None if y_flipped => {
892                    base_handle.set_offset(point(offset.x, Pixels::MIN));
893                    scroll_state.y_flipped = y_flipped;
894                }
895                _ => {}
896            }
897        }
898        self
899    }
900}
901
902impl InteractiveElement for UniformList {
903    fn interactivity(&mut self) -> &mut crate::Interactivity {
904        &mut self.interactivity
905    }
906}
907
908#[cfg(test)]
909mod test {
910    use crate::TestAppContext;
911
912    #[gpui::test]
913    fn scroll_to_bottom_in_flipped_list_does_not_overflow(cx: &mut TestAppContext) {
914        use crate::{
915            AppContext as _, Context, IntoElement, Render, Styled, UniformListScrollHandle, Window,
916            div, px, size, uniform_list,
917        };
918
919        struct FlippedRows(UniformListScrollHandle);
920        impl Render for FlippedRows {
921            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
922                uniform_list("rows", 47, |range, _, _| {
923                    range.map(|_| div().h(px(20.))).collect()
924                })
925                .track_scroll(&self.0)
926                .y_flipped(true)
927                .size_full()
928            }
929        }
930
931        let handle = UniformListScrollHandle::new();
932        let window = cx.open_window(size(px(200.), px(200.)), |_, _| FlippedRows(handle.clone()));
933        let mut draw = || {
934            cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx))
935                .unwrap();
936        };
937        draw();
938        handle.scroll_to_bottom();
939        draw();
940        assert_eq!(handle.0.borrow().base_handle.offset().y, px(0.));
941        assert!(handle.0.borrow().deferred_scroll_to_item.is_none());
942    }
943
944    #[gpui::test]
945    fn test_scroll_strategy_nearest(cx: &mut TestAppContext) {
946        use crate::{
947            Context, FocusHandle, ScrollStrategy, UniformListScrollHandle, Window, div, prelude::*,
948            px, uniform_list,
949        };
950        use std::ops::Range;
951
952        actions!(example, [SelectNext, SelectPrev]);
953
954        struct TestView {
955            index: usize,
956            length: usize,
957            scroll_handle: UniformListScrollHandle,
958            focus_handle: FocusHandle,
959            visible_range: Range<usize>,
960        }
961
962        impl TestView {
963            pub fn select_next(
964                &mut self,
965                _: &SelectNext,
966                window: &mut Window,
967                _: &mut Context<Self>,
968            ) {
969                if self.index + 1 == self.length {
970                    self.index = 0
971                } else {
972                    self.index += 1;
973                }
974                self.scroll_handle
975                    .scroll_to_item(self.index, ScrollStrategy::Nearest);
976                window.refresh();
977            }
978
979            pub fn select_previous(
980                &mut self,
981                _: &SelectPrev,
982                window: &mut Window,
983                _: &mut Context<Self>,
984            ) {
985                if self.index == 0 {
986                    self.index = self.length - 1
987                } else {
988                    self.index -= 1;
989                }
990                self.scroll_handle
991                    .scroll_to_item(self.index, ScrollStrategy::Nearest);
992                window.refresh();
993            }
994        }
995
996        impl Render for TestView {
997            fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
998                div()
999                    .id("list-example")
1000                    .track_focus(&self.focus_handle)
1001                    .on_action(cx.listener(Self::select_next))
1002                    .on_action(cx.listener(Self::select_previous))
1003                    .size_full()
1004                    .child(
1005                        uniform_list(
1006                            "entries",
1007                            self.length,
1008                            cx.processor(|this, range: Range<usize>, _window, _cx| {
1009                                this.visible_range = range.clone();
1010                                range
1011                                    .map(|ix| div().id(ix).h(px(20.0)).child(format!("Item {ix}")))
1012                                    .collect()
1013                            }),
1014                        )
1015                        .track_scroll(&self.scroll_handle)
1016                        .h(px(200.0)),
1017                    )
1018            }
1019        }
1020
1021        let (view, cx) = cx.add_window_view(|window, cx| {
1022            let focus_handle = cx.focus_handle();
1023            window.focus(&focus_handle, cx);
1024            TestView {
1025                scroll_handle: UniformListScrollHandle::new(),
1026                index: 0,
1027                focus_handle,
1028                length: 47,
1029                visible_range: 0..0,
1030            }
1031        });
1032
1033        // 10 out of 47 items are visible
1034
1035        // First 9 times selecting next item does not scroll
1036        for ix in 1..10 {
1037            cx.dispatch_action(SelectNext);
1038            view.read_with(cx, |view, _| {
1039                assert_eq!(view.index, ix);
1040                assert_eq!(view.visible_range, 0..10);
1041            })
1042        }
1043
1044        // Now each time the list scrolls down by 1
1045        for ix in 10..47 {
1046            cx.dispatch_action(SelectNext);
1047            view.read_with(cx, |view, _| {
1048                assert_eq!(view.index, ix);
1049                assert_eq!(view.visible_range, ix - 9..ix + 1);
1050            })
1051        }
1052
1053        // After the last item we move back to the start
1054        cx.dispatch_action(SelectNext);
1055        view.read_with(cx, |view, _| {
1056            assert_eq!(view.index, 0);
1057            assert_eq!(view.visible_range, 0..10);
1058        });
1059
1060        // Return to the last element
1061        cx.dispatch_action(SelectPrev);
1062        view.read_with(cx, |view, _| {
1063            assert_eq!(view.index, 46);
1064            assert_eq!(view.visible_range, 37..47);
1065        });
1066
1067        // First 9 times selecting previous does not scroll
1068        for ix in (37..46).rev() {
1069            cx.dispatch_action(SelectPrev);
1070            view.read_with(cx, |view, _| {
1071                assert_eq!(view.index, ix);
1072                assert_eq!(view.visible_range, 37..47);
1073            })
1074        }
1075
1076        // Now each time the list scrolls up by 1
1077        for ix in (0..37).rev() {
1078            cx.dispatch_action(SelectPrev);
1079            view.read_with(cx, |view, _| {
1080                assert_eq!(view.index, ix);
1081                assert_eq!(view.visible_range, ix..ix + 10);
1082            })
1083        }
1084    }
1085
1086    #[gpui::test]
1087    fn test_item_positions_far_down_long_list(cx: &mut TestAppContext) {
1088        use crate::{
1089            Bounds, Context, Pixels, UniformListScrollHandle, Window, canvas, point, prelude::*,
1090            px, uniform_list,
1091        };
1092        use std::{cell::RefCell, ops::Range, rc::Rc};
1093
1094        struct TestView {
1095            scroll_handle: UniformListScrollHandle,
1096            item_bounds: Rc<RefCell<Vec<(usize, Bounds<Pixels>)>>>,
1097        }
1098
1099        impl Render for TestView {
1100            fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1101                let item_bounds = self.item_bounds.clone();
1102                uniform_list(
1103                    "entries",
1104                    10_000_000,
1105                    cx.processor(move |_, range: Range<usize>, _window, _cx| {
1106                        range
1107                            .map(|ix| {
1108                                let item_bounds = item_bounds.clone();
1109                                canvas(
1110                                    move |bounds, _, _| item_bounds.borrow_mut().push((ix, bounds)),
1111                                    |_, _, _, _| {},
1112                                )
1113                                .w_full()
1114                                .h(px(24.0))
1115                            })
1116                            .collect()
1117                    }),
1118                )
1119                .track_scroll(&self.scroll_handle)
1120                .h(px(200.0))
1121            }
1122        }
1123
1124        let item_bounds = Rc::new(RefCell::new(Vec::new()));
1125        let (view, cx) = cx.add_window_view(|_, _| TestView {
1126            scroll_handle: UniformListScrollHandle::new(),
1127            item_bounds: item_bounds.clone(),
1128        });
1129
1130        // Past 2^27px, f32 can only represent multiples of 16px. This offset is 16px into item
1131        // 9_000_002, and the items must still be laid out 24px apart from that point.
1132        item_bounds.borrow_mut().clear();
1133        view.update(cx, |view, cx| {
1134            view.scroll_handle
1135                .0
1136                .borrow()
1137                .base_handle
1138                .set_offset(point(px(0.0), px(-216_000_064.0)));
1139            cx.notify();
1140        });
1141        cx.run_until_parked();
1142
1143        let item_tops = item_bounds
1144            .borrow()
1145            .iter()
1146            .map(|(ix, bounds)| (*ix, bounds.origin.y))
1147            .collect::<Vec<_>>();
1148        let expected_tops = (9_000_002..9_000_011)
1149            .map(|ix| (ix, px(-16.0 + 24.0 * (ix - 9_000_002) as f32)))
1150            .collect::<Vec<_>>();
1151        assert_eq!(item_tops, expected_tops);
1152    }
1153}