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