list.rs

  1//! A list element that can be used to render a large number of differently sized elements
  2//! efficiently. Clients of this API need to ensure that elements outside of the scrolled
  3//! area do not change their height for this element to function correctly. In order to minimize
  4//! re-renders, this element's state is stored intrusively on your own views, so that your code
  5//! can coordinate directly with the list element's cached state.
  6//!
  7//! If all of your elements are the same height, see [`UniformList`] for a simpler API
  8
  9use crate::{
 10    point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges,
 11    Element, FocusHandle, GlobalElementId, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent,
 12    Size, Style, StyleRefinement, Styled, WindowContext,
 13};
 14use collections::VecDeque;
 15use refineable::Refineable as _;
 16use std::{cell::RefCell, ops::Range, rc::Rc};
 17use sum_tree::{Bias, SumTree};
 18use taffy::style::Overflow;
 19
 20/// Construct a new list element
 21pub fn list(state: ListState) -> List {
 22    List {
 23        state,
 24        style: StyleRefinement::default(),
 25        sizing_behavior: ListSizingBehavior::default(),
 26    }
 27}
 28
 29/// A list element
 30pub struct List {
 31    state: ListState,
 32    style: StyleRefinement,
 33    sizing_behavior: ListSizingBehavior,
 34}
 35
 36impl List {
 37    /// Set the sizing behavior for the list.
 38    pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
 39        self.sizing_behavior = behavior;
 40        self
 41    }
 42}
 43
 44/// The list state that views must hold on behalf of the list element.
 45#[derive(Clone)]
 46pub struct ListState(Rc<RefCell<StateInner>>);
 47
 48struct StateInner {
 49    last_layout_bounds: Option<Bounds<Pixels>>,
 50    last_padding: Option<Edges<Pixels>>,
 51    render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
 52    items: SumTree<ListItem>,
 53    logical_scroll_top: Option<ListOffset>,
 54    alignment: ListAlignment,
 55    overdraw: Pixels,
 56    reset: bool,
 57    #[allow(clippy::type_complexity)]
 58    scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
 59}
 60
 61/// Whether the list is scrolling from top to bottom or bottom to top.
 62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
 63pub enum ListAlignment {
 64    /// The list is scrolling from top to bottom, like most lists.
 65    Top,
 66    /// The list is scrolling from bottom to top, like a chat log.
 67    Bottom,
 68}
 69
 70/// A scroll event that has been converted to be in terms of the list's items.
 71pub struct ListScrollEvent {
 72    /// The range of items currently visible in the list, after applying the scroll event.
 73    pub visible_range: Range<usize>,
 74
 75    /// The number of items that are currently visible in the list, after applying the scroll event.
 76    pub count: usize,
 77
 78    /// Whether the list has been scrolled.
 79    pub is_scrolled: bool,
 80}
 81
 82/// The sizing behavior to apply during layout.
 83#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
 84pub enum ListSizingBehavior {
 85    /// The list should calculate its size based on the size of its items.
 86    Infer,
 87    /// The list should not calculate a fixed size.
 88    #[default]
 89    Auto,
 90}
 91
 92struct LayoutItemsResponse {
 93    max_item_width: Pixels,
 94    scroll_top: ListOffset,
 95    item_layouts: VecDeque<ItemLayout>,
 96}
 97
 98struct ItemLayout {
 99    index: usize,
100    element: AnyElement,
101    size: Size<Pixels>,
102}
103
104/// Frame state used by the [List] element after layout.
105pub struct ListPrepaintState {
106    hitbox: Hitbox,
107    layout: LayoutItemsResponse,
108}
109
110#[derive(Clone)]
111enum ListItem {
112    Unmeasured {
113        focus_handle: Option<FocusHandle>,
114    },
115    Measured {
116        size: Size<Pixels>,
117        focus_handle: Option<FocusHandle>,
118    },
119}
120
121impl ListItem {
122    fn size(&self) -> Option<Size<Pixels>> {
123        if let ListItem::Measured { size, .. } = self {
124            Some(*size)
125        } else {
126            None
127        }
128    }
129
130    fn focus_handle(&self) -> Option<FocusHandle> {
131        match self {
132            ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
133                focus_handle.clone()
134            }
135        }
136    }
137
138    fn contains_focused(&self, cx: &WindowContext) -> bool {
139        match self {
140            ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
141                focus_handle
142                    .as_ref()
143                    .is_some_and(|handle| handle.contains_focused(cx))
144            }
145        }
146    }
147}
148
149#[derive(Clone, Debug, Default, PartialEq)]
150struct ListItemSummary {
151    count: usize,
152    rendered_count: usize,
153    unrendered_count: usize,
154    height: Pixels,
155    has_focus_handles: bool,
156}
157
158#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
159struct Count(usize);
160
161#[derive(Clone, Debug, Default)]
162struct Height(Pixels);
163
164impl ListState {
165    /// Construct a new list state, for storage on a view.
166    ///
167    /// The overdraw parameter controls how much extra space is rendered
168    /// above and below the visible area. Elements within this area will
169    /// be measured even though they are not visible. This can help ensure
170    /// that the list doesn't flicker or pop in when scrolling.
171    pub fn new<R>(
172        item_count: usize,
173        alignment: ListAlignment,
174        overdraw: Pixels,
175        render_item: R,
176    ) -> Self
177    where
178        R: 'static + FnMut(usize, &mut WindowContext) -> AnyElement,
179    {
180        let this = Self(Rc::new(RefCell::new(StateInner {
181            last_layout_bounds: None,
182            last_padding: None,
183            render_item: Box::new(render_item),
184            items: SumTree::new(),
185            logical_scroll_top: None,
186            alignment,
187            overdraw,
188            scroll_handler: None,
189            reset: false,
190        })));
191        this.splice(0..0, item_count);
192        this
193    }
194
195    /// Reset this instantiation of the list state.
196    ///
197    /// Note that this will cause scroll events to be dropped until the next paint.
198    pub fn reset(&self, element_count: usize) {
199        let old_count = {
200            let state = &mut *self.0.borrow_mut();
201            state.reset = true;
202            state.logical_scroll_top = None;
203            state.items.summary().count
204        };
205
206        self.splice(0..old_count, element_count);
207    }
208
209    /// The number of items in this list.
210    pub fn item_count(&self) -> usize {
211        self.0.borrow().items.summary().count
212    }
213
214    /// Inform the list state that the items in `old_range` have been replaced
215    /// by `count` new items that must be recalculated.
216    pub fn splice(&self, old_range: Range<usize>, count: usize) {
217        self.splice_focusable(old_range, (0..count).map(|_| None))
218    }
219
220    /// Register with the list state that the items in `old_range` have been replaced
221    /// by new items. As opposed to [`splice`], this method allows an iterator of optional focus handles
222    /// to be supplied to properly integrate with items in the list that can be focused. If a focused item
223    /// is scrolled out of view, the list will continue to render it to allow keyboard interaction.
224    pub fn splice_focusable(
225        &self,
226        old_range: Range<usize>,
227        focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
228    ) {
229        let state = &mut *self.0.borrow_mut();
230
231        let mut old_items = state.items.cursor::<Count>();
232        let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right, &());
233        old_items.seek_forward(&Count(old_range.end), Bias::Right, &());
234
235        let mut spliced_count = 0;
236        new_items.extend(
237            focus_handles.into_iter().map(|focus_handle| {
238                spliced_count += 1;
239                ListItem::Unmeasured { focus_handle }
240            }),
241            &(),
242        );
243        new_items.append(old_items.suffix(&()), &());
244        drop(old_items);
245        state.items = new_items;
246
247        if let Some(ListOffset {
248            item_ix,
249            offset_in_item,
250        }) = state.logical_scroll_top.as_mut()
251        {
252            if old_range.contains(item_ix) {
253                *item_ix = old_range.start;
254                *offset_in_item = px(0.);
255            } else if old_range.end <= *item_ix {
256                *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count;
257            }
258        }
259    }
260
261    /// Set a handler that will be called when the list is scrolled.
262    pub fn set_scroll_handler(
263        &self,
264        handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
265    ) {
266        self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
267    }
268
269    /// Get the current scroll offset, in terms of the list's items.
270    pub fn logical_scroll_top(&self) -> ListOffset {
271        self.0.borrow().logical_scroll_top()
272    }
273
274    /// Scroll the list to the given offset
275    pub fn scroll_to(&self, mut scroll_top: ListOffset) {
276        let state = &mut *self.0.borrow_mut();
277        let item_count = state.items.summary().count;
278        if scroll_top.item_ix >= item_count {
279            scroll_top.item_ix = item_count;
280            scroll_top.offset_in_item = px(0.);
281        }
282
283        state.logical_scroll_top = Some(scroll_top);
284    }
285
286    /// Scroll the list to the given item, such that the item is fully visible.
287    pub fn scroll_to_reveal_item(&self, ix: usize) {
288        let state = &mut *self.0.borrow_mut();
289
290        let mut scroll_top = state.logical_scroll_top();
291        let height = state
292            .last_layout_bounds
293            .map_or(px(0.), |bounds| bounds.size.height);
294        let padding = state.last_padding.unwrap_or_default();
295
296        if ix <= scroll_top.item_ix {
297            scroll_top.item_ix = ix;
298            scroll_top.offset_in_item = px(0.);
299        } else {
300            let mut cursor = state.items.cursor::<ListItemSummary>();
301            cursor.seek(&Count(ix + 1), Bias::Right, &());
302            let bottom = cursor.start().height + padding.top;
303            let goal_top = px(0.).max(bottom - height + padding.bottom);
304
305            cursor.seek(&Height(goal_top), Bias::Left, &());
306            let start_ix = cursor.start().count;
307            let start_item_top = cursor.start().height;
308
309            if start_ix >= scroll_top.item_ix {
310                scroll_top.item_ix = start_ix;
311                scroll_top.offset_in_item = goal_top - start_item_top;
312            }
313        }
314
315        state.logical_scroll_top = Some(scroll_top);
316    }
317
318    /// Get the bounds for the given item in window coordinates, if it's
319    /// been rendered.
320    pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
321        let state = &*self.0.borrow();
322
323        let bounds = state.last_layout_bounds.unwrap_or_default();
324        let scroll_top = state.logical_scroll_top();
325        if ix < scroll_top.item_ix {
326            return None;
327        }
328
329        let mut cursor = state.items.cursor::<(Count, Height)>();
330        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
331
332        let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item;
333
334        cursor.seek_forward(&Count(ix), Bias::Right, &());
335        if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
336            let &(Count(count), Height(top)) = cursor.start();
337            if count == ix {
338                let top = bounds.top() + top - scroll_top;
339                return Some(Bounds::from_corners(
340                    point(bounds.left(), top),
341                    point(bounds.right(), top + size.height),
342                ));
343            }
344        }
345        None
346    }
347}
348
349impl StateInner {
350    fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
351        let mut cursor = self.items.cursor::<ListItemSummary>();
352        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
353        let start_y = cursor.start().height + scroll_top.offset_in_item;
354        cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
355        scroll_top.item_ix..cursor.start().count + 1
356    }
357
358    fn scroll(
359        &mut self,
360        scroll_top: &ListOffset,
361        height: Pixels,
362        delta: Point<Pixels>,
363        cx: &mut WindowContext,
364    ) {
365        // Drop scroll events after a reset, since we can't calculate
366        // the new logical scroll top without the item heights
367        if self.reset {
368            return;
369        }
370
371        let padding = self.last_padding.unwrap_or_default();
372        let scroll_max =
373            (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
374        let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
375            .max(px(0.))
376            .min(scroll_max);
377
378        if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
379            self.logical_scroll_top = None;
380        } else {
381            let mut cursor = self.items.cursor::<ListItemSummary>();
382            cursor.seek(&Height(new_scroll_top), Bias::Right, &());
383            let item_ix = cursor.start().count;
384            let offset_in_item = new_scroll_top - cursor.start().height;
385            self.logical_scroll_top = Some(ListOffset {
386                item_ix,
387                offset_in_item,
388            });
389        }
390
391        if self.scroll_handler.is_some() {
392            let visible_range = self.visible_range(height, scroll_top);
393            self.scroll_handler.as_mut().unwrap()(
394                &ListScrollEvent {
395                    visible_range,
396                    count: self.items.summary().count,
397                    is_scrolled: self.logical_scroll_top.is_some(),
398                },
399                cx,
400            );
401        }
402
403        cx.refresh();
404    }
405
406    fn logical_scroll_top(&self) -> ListOffset {
407        self.logical_scroll_top
408            .unwrap_or_else(|| match self.alignment {
409                ListAlignment::Top => ListOffset {
410                    item_ix: 0,
411                    offset_in_item: px(0.),
412                },
413                ListAlignment::Bottom => ListOffset {
414                    item_ix: self.items.summary().count,
415                    offset_in_item: px(0.),
416                },
417            })
418    }
419
420    fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
421        let mut cursor = self.items.cursor::<ListItemSummary>();
422        cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
423        cursor.start().height + logical_scroll_top.offset_in_item
424    }
425
426    fn layout_items(
427        &mut self,
428        available_width: Option<Pixels>,
429        available_height: Pixels,
430        padding: &Edges<Pixels>,
431        cx: &mut WindowContext,
432    ) -> LayoutItemsResponse {
433        let old_items = self.items.clone();
434        let mut measured_items = VecDeque::new();
435        let mut item_layouts = VecDeque::new();
436        let mut rendered_height = padding.top;
437        let mut max_item_width = px(0.);
438        let mut scroll_top = self.logical_scroll_top();
439        let mut rendered_focused_item = false;
440
441        let available_item_space = size(
442            available_width.map_or(AvailableSpace::MinContent, |width| {
443                AvailableSpace::Definite(width)
444            }),
445            AvailableSpace::MinContent,
446        );
447
448        let mut cursor = old_items.cursor::<Count>();
449
450        // Render items after the scroll top, including those in the trailing overdraw
451        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
452        for (ix, item) in cursor.by_ref().enumerate() {
453            let visible_height = rendered_height - scroll_top.offset_in_item;
454            if visible_height >= available_height + self.overdraw {
455                break;
456            }
457
458            // Use the previously cached height and focus handle if available
459            let mut size = item.size();
460
461            // If we're within the visible area or the height wasn't cached, render and measure the item's element
462            if visible_height < available_height || size.is_none() {
463                let item_index = scroll_top.item_ix + ix;
464                let mut element = (self.render_item)(item_index, cx);
465                let element_size = element.layout_as_root(available_item_space, cx);
466                size = Some(element_size);
467                if visible_height < available_height {
468                    item_layouts.push_back(ItemLayout {
469                        index: item_index,
470                        element,
471                        size: element_size,
472                    });
473                    if item.contains_focused(cx) {
474                        rendered_focused_item = true;
475                    }
476                }
477            }
478
479            let size = size.unwrap();
480            rendered_height += size.height;
481            max_item_width = max_item_width.max(size.width);
482            measured_items.push_back(ListItem::Measured {
483                size,
484                focus_handle: item.focus_handle(),
485            });
486        }
487        rendered_height += padding.bottom;
488
489        // Prepare to start walking upward from the item at the scroll top.
490        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
491
492        // If the rendered items do not fill the visible region, then adjust
493        // the scroll top upward.
494        if rendered_height - scroll_top.offset_in_item < available_height {
495            while rendered_height < available_height {
496                cursor.prev(&());
497                if let Some(item) = cursor.item() {
498                    let item_index = cursor.start().0;
499                    let mut element = (self.render_item)(item_index, cx);
500                    let element_size = element.layout_as_root(available_item_space, cx);
501                    let focus_handle = item.focus_handle();
502                    rendered_height += element_size.height;
503                    measured_items.push_front(ListItem::Measured {
504                        size: element_size,
505                        focus_handle,
506                    });
507                    item_layouts.push_front(ItemLayout {
508                        index: item_index,
509                        element,
510                        size: element_size,
511                    });
512                    if item.contains_focused(cx) {
513                        rendered_focused_item = true;
514                    }
515                } else {
516                    break;
517                }
518            }
519
520            scroll_top = ListOffset {
521                item_ix: cursor.start().0,
522                offset_in_item: rendered_height - available_height,
523            };
524
525            match self.alignment {
526                ListAlignment::Top => {
527                    scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
528                    self.logical_scroll_top = Some(scroll_top);
529                }
530                ListAlignment::Bottom => {
531                    scroll_top = ListOffset {
532                        item_ix: cursor.start().0,
533                        offset_in_item: rendered_height - available_height,
534                    };
535                    self.logical_scroll_top = None;
536                }
537            };
538        }
539
540        // Measure items in the leading overdraw
541        let mut leading_overdraw = scroll_top.offset_in_item;
542        while leading_overdraw < self.overdraw {
543            cursor.prev(&());
544            if let Some(item) = cursor.item() {
545                let size = if let ListItem::Measured { size, .. } = item {
546                    *size
547                } else {
548                    let mut element = (self.render_item)(cursor.start().0, cx);
549                    element.layout_as_root(available_item_space, cx)
550                };
551
552                leading_overdraw += size.height;
553                measured_items.push_front(ListItem::Measured {
554                    size,
555                    focus_handle: item.focus_handle(),
556                });
557            } else {
558                break;
559            }
560        }
561
562        let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
563        let mut cursor = old_items.cursor::<Count>();
564        let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &());
565        new_items.extend(measured_items, &());
566        cursor.seek(&Count(measured_range.end), Bias::Right, &());
567        new_items.append(cursor.suffix(&()), &());
568        self.items = new_items;
569
570        // If none of the visible items are focused, check if an off-screen item is focused
571        // and include it to be rendered after the visible items so keyboard interaction continues
572        // to work for it.
573        if !rendered_focused_item {
574            let mut cursor = self
575                .items
576                .filter::<_, Count>(|summary| summary.has_focus_handles);
577            cursor.next(&());
578            while let Some(item) = cursor.item() {
579                if item.contains_focused(cx) {
580                    let item_index = cursor.start().0;
581                    let mut element = (self.render_item)(cursor.start().0, cx);
582                    let size = element.layout_as_root(available_item_space, cx);
583                    item_layouts.push_back(ItemLayout {
584                        index: item_index,
585                        element,
586                        size,
587                    });
588                    break;
589                }
590                cursor.next(&());
591            }
592        }
593
594        LayoutItemsResponse {
595            max_item_width,
596            scroll_top,
597            item_layouts,
598        }
599    }
600
601    fn prepaint_items(
602        &mut self,
603        bounds: Bounds<Pixels>,
604        padding: Edges<Pixels>,
605        autoscroll: bool,
606        cx: &mut WindowContext,
607    ) -> Result<LayoutItemsResponse, ListOffset> {
608        cx.transact(|cx| {
609            let mut layout_response =
610                self.layout_items(Some(bounds.size.width), bounds.size.height, &padding, cx);
611
612            // Avoid honoring autoscroll requests from elements other than our children.
613            cx.take_autoscroll();
614
615            // Only paint the visible items, if there is actually any space for them (taking padding into account)
616            if bounds.size.height > padding.top + padding.bottom {
617                let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
618                item_origin.y -= layout_response.scroll_top.offset_in_item;
619                for item in &mut layout_response.item_layouts {
620                    cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
621                        item.element.prepaint_at(item_origin, cx);
622                    });
623
624                    if let Some(autoscroll_bounds) = cx.take_autoscroll() {
625                        if autoscroll {
626                            if autoscroll_bounds.top() < bounds.top() {
627                                return Err(ListOffset {
628                                    item_ix: item.index,
629                                    offset_in_item: autoscroll_bounds.top() - item_origin.y,
630                                });
631                            } else if autoscroll_bounds.bottom() > bounds.bottom() {
632                                let mut cursor = self.items.cursor::<Count>();
633                                cursor.seek(&Count(item.index), Bias::Right, &());
634                                let mut height = bounds.size.height - padding.top - padding.bottom;
635
636                                // Account for the height of the element down until the autoscroll bottom.
637                                height -= autoscroll_bounds.bottom() - item_origin.y;
638
639                                // Keep decreasing the scroll top until we fill all the available space.
640                                while height > Pixels::ZERO {
641                                    cursor.prev(&());
642                                    let Some(item) = cursor.item() else { break };
643
644                                    let size = item.size().unwrap_or_else(|| {
645                                        let mut item = (self.render_item)(cursor.start().0, cx);
646                                        let item_available_size = size(
647                                            bounds.size.width.into(),
648                                            AvailableSpace::MinContent,
649                                        );
650                                        item.layout_as_root(item_available_size, cx)
651                                    });
652                                    height -= size.height;
653                                }
654
655                                return Err(ListOffset {
656                                    item_ix: cursor.start().0,
657                                    offset_in_item: if height < Pixels::ZERO {
658                                        -height
659                                    } else {
660                                        Pixels::ZERO
661                                    },
662                                });
663                            }
664                        }
665                    }
666
667                    item_origin.y += item.size.height;
668                }
669            } else {
670                layout_response.item_layouts.clear();
671            }
672
673            Ok(layout_response)
674        })
675    }
676}
677
678impl std::fmt::Debug for ListItem {
679    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
680        match self {
681            Self::Unmeasured { .. } => write!(f, "Unrendered"),
682            Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
683        }
684    }
685}
686
687/// An offset into the list's items, in terms of the item index and the number
688/// of pixels off the top left of the item.
689#[derive(Debug, Clone, Copy, Default)]
690pub struct ListOffset {
691    /// The index of an item in the list
692    pub item_ix: usize,
693    /// The number of pixels to offset from the item index.
694    pub offset_in_item: Pixels,
695}
696
697impl Element for List {
698    type RequestLayoutState = ();
699    type PrepaintState = ListPrepaintState;
700
701    fn id(&self) -> Option<crate::ElementId> {
702        None
703    }
704
705    fn request_layout(
706        &mut self,
707        _id: Option<&GlobalElementId>,
708        cx: &mut crate::WindowContext,
709    ) -> (crate::LayoutId, Self::RequestLayoutState) {
710        let layout_id = match self.sizing_behavior {
711            ListSizingBehavior::Infer => {
712                let mut style = Style::default();
713                style.overflow.y = Overflow::Scroll;
714                style.refine(&self.style);
715                cx.with_text_style(style.text_style().cloned(), |cx| {
716                    let state = &mut *self.state.0.borrow_mut();
717
718                    let available_height = if let Some(last_bounds) = state.last_layout_bounds {
719                        last_bounds.size.height
720                    } else {
721                        // If we don't have the last layout bounds (first render),
722                        // we might just use the overdraw value as the available height to layout enough items.
723                        state.overdraw
724                    };
725                    let padding = style.padding.to_pixels(
726                        state.last_layout_bounds.unwrap_or_default().size.into(),
727                        cx.rem_size(),
728                    );
729
730                    let layout_response = state.layout_items(None, available_height, &padding, cx);
731                    let max_element_width = layout_response.max_item_width;
732
733                    let summary = state.items.summary();
734                    let total_height = summary.height;
735
736                    cx.request_measured_layout(
737                        style,
738                        move |known_dimensions, available_space, _cx| {
739                            let width =
740                                known_dimensions
741                                    .width
742                                    .unwrap_or(match available_space.width {
743                                        AvailableSpace::Definite(x) => x,
744                                        AvailableSpace::MinContent | AvailableSpace::MaxContent => {
745                                            max_element_width
746                                        }
747                                    });
748                            let height = match available_space.height {
749                                AvailableSpace::Definite(height) => total_height.min(height),
750                                AvailableSpace::MinContent | AvailableSpace::MaxContent => {
751                                    total_height
752                                }
753                            };
754                            size(width, height)
755                        },
756                    )
757                })
758            }
759            ListSizingBehavior::Auto => {
760                let mut style = Style::default();
761                style.refine(&self.style);
762                cx.with_text_style(style.text_style().cloned(), |cx| {
763                    cx.request_layout(style, None)
764                })
765            }
766        };
767        (layout_id, ())
768    }
769
770    fn prepaint(
771        &mut self,
772        _id: Option<&GlobalElementId>,
773        bounds: Bounds<Pixels>,
774        _: &mut Self::RequestLayoutState,
775        cx: &mut WindowContext,
776    ) -> ListPrepaintState {
777        let state = &mut *self.state.0.borrow_mut();
778        state.reset = false;
779
780        let mut style = Style::default();
781        style.refine(&self.style);
782
783        let hitbox = cx.insert_hitbox(bounds, false);
784
785        // If the width of the list has changed, invalidate all cached item heights
786        if state.last_layout_bounds.map_or(true, |last_bounds| {
787            last_bounds.size.width != bounds.size.width
788        }) {
789            let new_items = SumTree::from_iter(
790                state.items.iter().map(|item| ListItem::Unmeasured {
791                    focus_handle: item.focus_handle(),
792                }),
793                &(),
794            );
795
796            state.items = new_items;
797        }
798
799        let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
800        let layout = match state.prepaint_items(bounds, padding, true, cx) {
801            Ok(layout) => layout,
802            Err(autoscroll_request) => {
803                state.logical_scroll_top = Some(autoscroll_request);
804                state.prepaint_items(bounds, padding, false, cx).unwrap()
805            }
806        };
807
808        state.last_layout_bounds = Some(bounds);
809        state.last_padding = Some(padding);
810        ListPrepaintState { hitbox, layout }
811    }
812
813    fn paint(
814        &mut self,
815        _id: Option<&GlobalElementId>,
816        bounds: Bounds<crate::Pixels>,
817        _: &mut Self::RequestLayoutState,
818        prepaint: &mut Self::PrepaintState,
819        cx: &mut crate::WindowContext,
820    ) {
821        cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
822            for item in &mut prepaint.layout.item_layouts {
823                item.element.paint(cx);
824            }
825        });
826
827        let list_state = self.state.clone();
828        let height = bounds.size.height;
829        let scroll_top = prepaint.layout.scroll_top;
830        let hitbox_id = prepaint.hitbox.id;
831        cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
832            if phase == DispatchPhase::Bubble && hitbox_id.is_hovered(cx) {
833                list_state.0.borrow_mut().scroll(
834                    &scroll_top,
835                    height,
836                    event.delta.pixel_delta(px(20.)),
837                    cx,
838                )
839            }
840        });
841    }
842}
843
844impl IntoElement for List {
845    type Element = Self;
846
847    fn into_element(self) -> Self::Element {
848        self
849    }
850}
851
852impl Styled for List {
853    fn style(&mut self) -> &mut StyleRefinement {
854        &mut self.style
855    }
856}
857
858impl sum_tree::Item for ListItem {
859    type Summary = ListItemSummary;
860
861    fn summary(&self) -> Self::Summary {
862        match self {
863            ListItem::Unmeasured { focus_handle } => ListItemSummary {
864                count: 1,
865                rendered_count: 0,
866                unrendered_count: 1,
867                height: px(0.),
868                has_focus_handles: focus_handle.is_some(),
869            },
870            ListItem::Measured {
871                size, focus_handle, ..
872            } => ListItemSummary {
873                count: 1,
874                rendered_count: 1,
875                unrendered_count: 0,
876                height: size.height,
877                has_focus_handles: focus_handle.is_some(),
878            },
879        }
880    }
881}
882
883impl sum_tree::Summary for ListItemSummary {
884    type Context = ();
885
886    fn add_summary(&mut self, summary: &Self, _: &()) {
887        self.count += summary.count;
888        self.rendered_count += summary.rendered_count;
889        self.unrendered_count += summary.unrendered_count;
890        self.height += summary.height;
891        self.has_focus_handles |= summary.has_focus_handles;
892    }
893}
894
895impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
896    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
897        self.0 += summary.count;
898    }
899}
900
901impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
902    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
903        self.0 += summary.height;
904    }
905}
906
907impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
908    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
909        self.0.partial_cmp(&other.count).unwrap()
910    }
911}
912
913impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
914    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
915        self.0.partial_cmp(&other.height).unwrap()
916    }
917}
918
919#[cfg(test)]
920mod test {
921
922    use gpui::{ScrollDelta, ScrollWheelEvent};
923
924    use crate::{self as gpui, TestAppContext};
925
926    #[gpui::test]
927    fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
928        use crate::{div, list, point, px, size, Element, ListState, Styled};
929
930        let cx = cx.add_empty_window();
931
932        let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _| {
933            div().h(px(10.)).w_full().into_any()
934        });
935
936        // Ensure that the list is scrolled to the top
937        state.scroll_to(gpui::ListOffset {
938            item_ix: 0,
939            offset_in_item: px(0.0),
940        });
941
942        // Paint
943        cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_| {
944            list(state.clone()).w_full().h_full()
945        });
946
947        // Reset
948        state.reset(5);
949
950        // And then receive a scroll event _before_ the next paint
951        cx.simulate_event(ScrollWheelEvent {
952            position: point(px(1.), px(1.)),
953            delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
954            ..Default::default()
955        });
956
957        // Scroll position should stay at the top of the list
958        assert_eq!(state.logical_scroll_top().item_ix, 0);
959        assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
960    }
961}