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, ElementContext, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style,
 12    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)]
 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    available_item_space: Size<AvailableSpace>,
 96    item_elements: VecDeque<AnyElement>,
 97}
 98
 99#[derive(Clone)]
100enum ListItem {
101    Unrendered,
102    Rendered { size: Size<Pixels> },
103}
104
105#[derive(Clone, Debug, Default, PartialEq)]
106struct ListItemSummary {
107    count: usize,
108    rendered_count: usize,
109    unrendered_count: usize,
110    height: Pixels,
111}
112
113#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
114struct Count(usize);
115
116#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
117struct RenderedCount(usize);
118
119#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
120struct UnrenderedCount(usize);
121
122#[derive(Clone, Debug, Default)]
123struct Height(Pixels);
124
125impl ListState {
126    /// Construct a new list state, for storage on a view.
127    ///
128    /// the overdraw parameter controls how much extra space is rendered
129    /// above and below the visible area. This can help ensure that the list
130    /// doesn't flicker or pop in when scrolling.
131    pub fn new<F>(
132        element_count: usize,
133        orientation: ListAlignment,
134        overdraw: Pixels,
135        render_item: F,
136    ) -> Self
137    where
138        F: 'static + FnMut(usize, &mut WindowContext) -> AnyElement,
139    {
140        let mut items = SumTree::new();
141        items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
142        Self(Rc::new(RefCell::new(StateInner {
143            last_layout_bounds: None,
144            last_padding: None,
145            render_item: Box::new(render_item),
146            items,
147            logical_scroll_top: None,
148            alignment: orientation,
149            overdraw,
150            scroll_handler: None,
151            reset: false,
152        })))
153    }
154
155    /// Reset this instantiation of the list state.
156    ///
157    /// Note that this will cause scroll events to be dropped until the next paint.
158    pub fn reset(&self, element_count: usize) {
159        let state = &mut *self.0.borrow_mut();
160        state.reset = true;
161
162        state.logical_scroll_top = None;
163        state.items = SumTree::new();
164        state
165            .items
166            .extend((0..element_count).map(|_| ListItem::Unrendered), &());
167    }
168
169    /// The number of items in this list.
170    pub fn item_count(&self) -> usize {
171        self.0.borrow().items.summary().count
172    }
173
174    /// Register with the list state that the items in `old_range` have been replaced
175    /// by `count` new items that must be recalculated.
176    pub fn splice(&self, old_range: Range<usize>, count: usize) {
177        let state = &mut *self.0.borrow_mut();
178
179        if let Some(ListOffset {
180            item_ix,
181            offset_in_item,
182        }) = state.logical_scroll_top.as_mut()
183        {
184            if old_range.contains(item_ix) {
185                *item_ix = old_range.start;
186                *offset_in_item = px(0.);
187            } else if old_range.end <= *item_ix {
188                *item_ix = *item_ix - (old_range.end - old_range.start) + count;
189            }
190        }
191
192        let mut old_heights = state.items.cursor::<Count>();
193        let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
194        old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
195
196        new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
197        new_heights.append(old_heights.suffix(&()), &());
198        drop(old_heights);
199        state.items = new_heights;
200    }
201
202    /// Set a handler that will be called when the list is scrolled.
203    pub fn set_scroll_handler(
204        &self,
205        handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
206    ) {
207        self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
208    }
209
210    /// Get the current scroll offset, in terms of the list's items.
211    pub fn logical_scroll_top(&self) -> ListOffset {
212        self.0.borrow().logical_scroll_top()
213    }
214
215    /// Scroll the list to the given offset
216    pub fn scroll_to(&self, mut scroll_top: ListOffset) {
217        let state = &mut *self.0.borrow_mut();
218        let item_count = state.items.summary().count;
219        if scroll_top.item_ix >= item_count {
220            scroll_top.item_ix = item_count;
221            scroll_top.offset_in_item = px(0.);
222        }
223
224        state.logical_scroll_top = Some(scroll_top);
225    }
226
227    /// Scroll the list to the given item, such that the item is fully visible.
228    pub fn scroll_to_reveal_item(&self, ix: usize) {
229        let state = &mut *self.0.borrow_mut();
230
231        let mut scroll_top = state.logical_scroll_top();
232        let height = state
233            .last_layout_bounds
234            .map_or(px(0.), |bounds| bounds.size.height);
235        let padding = state.last_padding.unwrap_or_default();
236
237        if ix <= scroll_top.item_ix {
238            scroll_top.item_ix = ix;
239            scroll_top.offset_in_item = px(0.);
240        } else {
241            let mut cursor = state.items.cursor::<ListItemSummary>();
242            cursor.seek(&Count(ix + 1), Bias::Right, &());
243            let bottom = cursor.start().height + padding.top;
244            let goal_top = px(0.).max(bottom - height);
245
246            cursor.seek(&Height(goal_top), Bias::Left, &());
247            let start_ix = cursor.start().count;
248            let start_item_top = cursor.start().height;
249
250            if start_ix >= scroll_top.item_ix {
251                scroll_top.item_ix = start_ix;
252                scroll_top.offset_in_item = goal_top - start_item_top;
253            }
254        }
255
256        state.logical_scroll_top = Some(scroll_top);
257    }
258
259    /// Get the bounds for the given item in window coordinates, if it's
260    /// been rendered.
261    pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
262        let state = &*self.0.borrow();
263
264        let bounds = state.last_layout_bounds.unwrap_or_default();
265        let scroll_top = state.logical_scroll_top();
266        if ix < scroll_top.item_ix {
267            return None;
268        }
269
270        let mut cursor = state.items.cursor::<(Count, Height)>();
271        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
272
273        let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item;
274
275        cursor.seek_forward(&Count(ix), Bias::Right, &());
276        if let Some(&ListItem::Rendered { size }) = cursor.item() {
277            let &(Count(count), Height(top)) = cursor.start();
278            if count == ix {
279                let top = bounds.top() + top - scroll_top;
280                return Some(Bounds::from_corners(
281                    point(bounds.left(), top),
282                    point(bounds.right(), top + size.height),
283                ));
284            }
285        }
286        None
287    }
288}
289
290impl StateInner {
291    fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
292        let mut cursor = self.items.cursor::<ListItemSummary>();
293        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
294        let start_y = cursor.start().height + scroll_top.offset_in_item;
295        cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
296        scroll_top.item_ix..cursor.start().count + 1
297    }
298
299    fn scroll(
300        &mut self,
301        scroll_top: &ListOffset,
302        height: Pixels,
303        delta: Point<Pixels>,
304        cx: &mut WindowContext,
305        padding: Edges<Pixels>,
306    ) {
307        // Drop scroll events after a reset, since we can't calculate
308        // the new logical scroll top without the item heights
309        if self.reset {
310            return;
311        }
312
313        let scroll_max =
314            (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
315        let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
316            .max(px(0.))
317            .min(scroll_max);
318
319        if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
320            self.logical_scroll_top = None;
321        } else {
322            let mut cursor = self.items.cursor::<ListItemSummary>();
323            cursor.seek(&Height(new_scroll_top), Bias::Right, &());
324            let item_ix = cursor.start().count;
325            let offset_in_item = new_scroll_top - cursor.start().height;
326            self.logical_scroll_top = Some(ListOffset {
327                item_ix,
328                offset_in_item,
329            });
330        }
331
332        if self.scroll_handler.is_some() {
333            let visible_range = self.visible_range(height, scroll_top);
334            self.scroll_handler.as_mut().unwrap()(
335                &ListScrollEvent {
336                    visible_range,
337                    count: self.items.summary().count,
338                    is_scrolled: self.logical_scroll_top.is_some(),
339                },
340                cx,
341            );
342        }
343
344        cx.refresh();
345    }
346
347    fn logical_scroll_top(&self) -> ListOffset {
348        self.logical_scroll_top
349            .unwrap_or_else(|| match self.alignment {
350                ListAlignment::Top => ListOffset {
351                    item_ix: 0,
352                    offset_in_item: px(0.),
353                },
354                ListAlignment::Bottom => ListOffset {
355                    item_ix: self.items.summary().count,
356                    offset_in_item: px(0.),
357                },
358            })
359    }
360
361    fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
362        let mut cursor = self.items.cursor::<ListItemSummary>();
363        cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
364        cursor.start().height + logical_scroll_top.offset_in_item
365    }
366
367    fn layout_items(
368        &mut self,
369        available_width: Option<Pixels>,
370        available_height: Pixels,
371        padding: &Edges<Pixels>,
372        cx: &mut ElementContext,
373    ) -> LayoutItemsResponse {
374        let old_items = self.items.clone();
375        let mut measured_items = VecDeque::new();
376        let mut item_elements = VecDeque::new();
377        let mut rendered_height = padding.top;
378        let mut max_item_width = px(0.);
379        let mut scroll_top = self.logical_scroll_top();
380
381        let available_item_space = size(
382            available_width.map_or(AvailableSpace::MinContent, |width| {
383                AvailableSpace::Definite(width)
384            }),
385            AvailableSpace::MinContent,
386        );
387
388        let mut cursor = old_items.cursor::<Count>();
389
390        // Render items after the scroll top, including those in the trailing overdraw
391        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
392        for (ix, item) in cursor.by_ref().enumerate() {
393            let visible_height = rendered_height - scroll_top.offset_in_item;
394            if visible_height >= available_height + self.overdraw {
395                break;
396            }
397
398            // Use the previously cached height if available
399            let mut size = if let ListItem::Rendered { size } = item {
400                Some(*size)
401            } else {
402                None
403            };
404
405            // If we're within the visible area or the height wasn't cached, render and measure the item's element
406            if visible_height < available_height || size.is_none() {
407                let mut element = (self.render_item)(scroll_top.item_ix + ix, cx);
408                let element_size = element.measure(available_item_space, cx);
409                size = Some(element_size);
410                if visible_height < available_height {
411                    item_elements.push_back(element);
412                }
413            }
414
415            let size = size.unwrap();
416            rendered_height += size.height;
417            max_item_width = max_item_width.max(size.width);
418            measured_items.push_back(ListItem::Rendered { size });
419        }
420        rendered_height += padding.bottom;
421
422        // Prepare to start walking upward from the item at the scroll top.
423        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
424
425        // If the rendered items do not fill the visible region, then adjust
426        // the scroll top upward.
427        if rendered_height - scroll_top.offset_in_item < available_height {
428            while rendered_height < available_height {
429                cursor.prev(&());
430                if cursor.item().is_some() {
431                    let mut element = (self.render_item)(cursor.start().0, cx);
432                    let element_size = element.measure(available_item_space, cx);
433
434                    rendered_height += element_size.height;
435                    measured_items.push_front(ListItem::Rendered { size: element_size });
436                    item_elements.push_front(element)
437                } else {
438                    break;
439                }
440            }
441
442            scroll_top = ListOffset {
443                item_ix: cursor.start().0,
444                offset_in_item: rendered_height - available_height,
445            };
446
447            match self.alignment {
448                ListAlignment::Top => {
449                    scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
450                    self.logical_scroll_top = Some(scroll_top);
451                }
452                ListAlignment::Bottom => {
453                    scroll_top = ListOffset {
454                        item_ix: cursor.start().0,
455                        offset_in_item: rendered_height - available_height,
456                    };
457                    self.logical_scroll_top = None;
458                }
459            };
460        }
461
462        // Measure items in the leading overdraw
463        let mut leading_overdraw = scroll_top.offset_in_item;
464        while leading_overdraw < self.overdraw {
465            cursor.prev(&());
466            if let Some(item) = cursor.item() {
467                let size = if let ListItem::Rendered { size } = item {
468                    *size
469                } else {
470                    let mut element = (self.render_item)(cursor.start().0, cx);
471                    element.measure(available_item_space, cx)
472                };
473
474                leading_overdraw += size.height;
475                measured_items.push_front(ListItem::Rendered { size });
476            } else {
477                break;
478            }
479        }
480
481        let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
482        let mut cursor = old_items.cursor::<Count>();
483        let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &());
484        new_items.extend(measured_items, &());
485        cursor.seek(&Count(measured_range.end), Bias::Right, &());
486        new_items.append(cursor.suffix(&()), &());
487
488        self.items = new_items;
489
490        LayoutItemsResponse {
491            max_item_width,
492            scroll_top,
493            available_item_space,
494            item_elements,
495        }
496    }
497}
498
499impl std::fmt::Debug for ListItem {
500    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501        match self {
502            Self::Unrendered => write!(f, "Unrendered"),
503            Self::Rendered { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
504        }
505    }
506}
507
508/// An offset into the list's items, in terms of the item index and the number
509/// of pixels off the top left of the item.
510#[derive(Debug, Clone, Copy, Default)]
511pub struct ListOffset {
512    /// The index of an item in the list
513    pub item_ix: usize,
514    /// The number of pixels to offset from the item index.
515    pub offset_in_item: Pixels,
516}
517
518impl Element for List {
519    type State = ();
520
521    fn request_layout(
522        &mut self,
523        _state: Option<Self::State>,
524        cx: &mut crate::ElementContext,
525    ) -> (crate::LayoutId, Self::State) {
526        let layout_id = match self.sizing_behavior {
527            ListSizingBehavior::Infer => {
528                let mut style = Style::default();
529                style.overflow.y = Overflow::Scroll;
530                style.refine(&self.style);
531                cx.with_text_style(style.text_style().cloned(), |cx| {
532                    let state = &mut *self.state.0.borrow_mut();
533
534                    let available_height = if let Some(last_bounds) = state.last_layout_bounds {
535                        last_bounds.size.height
536                    } else {
537                        // If we don't have the last layout bounds (first render),
538                        // we might just use the overdraw value as the available height to layout enough items.
539                        state.overdraw
540                    };
541                    let padding = style.padding.to_pixels(
542                        state.last_layout_bounds.unwrap_or_default().size.into(),
543                        cx.rem_size(),
544                    );
545
546                    let layout_response = state.layout_items(None, available_height, &padding, cx);
547                    let max_element_width = layout_response.max_item_width;
548
549                    let summary = state.items.summary();
550                    let total_height = summary.height;
551                    let all_rendered = summary.unrendered_count == 0;
552
553                    if all_rendered {
554                        cx.request_measured_layout(
555                            style,
556                            move |known_dimensions, available_space, _cx| {
557                                let width = known_dimensions.width.unwrap_or(match available_space
558                                    .width
559                                {
560                                    AvailableSpace::Definite(x) => x,
561                                    AvailableSpace::MinContent | AvailableSpace::MaxContent => {
562                                        max_element_width
563                                    }
564                                });
565                                let height = match available_space.height {
566                                    AvailableSpace::Definite(height) => total_height.min(height),
567                                    AvailableSpace::MinContent | AvailableSpace::MaxContent => {
568                                        total_height
569                                    }
570                                };
571                                size(width, height)
572                            },
573                        )
574                    } else {
575                        cx.request_layout(&style, None)
576                    }
577                })
578            }
579            ListSizingBehavior::Auto => {
580                let mut style = Style::default();
581                style.refine(&self.style);
582                cx.with_text_style(style.text_style().cloned(), |cx| {
583                    cx.request_layout(&style, None)
584                })
585            }
586        };
587        (layout_id, ())
588    }
589
590    fn paint(
591        &mut self,
592        bounds: Bounds<crate::Pixels>,
593        _state: &mut Self::State,
594        cx: &mut crate::ElementContext,
595    ) {
596        let state = &mut *self.state.0.borrow_mut();
597        state.reset = false;
598
599        let mut style = Style::default();
600        style.refine(&self.style);
601
602        // If the width of the list has changed, invalidate all cached item heights
603        if state.last_layout_bounds.map_or(true, |last_bounds| {
604            last_bounds.size.width != bounds.size.width
605        }) {
606            state.items = SumTree::from_iter(
607                (0..state.items.summary().count).map(|_| ListItem::Unrendered),
608                &(),
609            )
610        }
611
612        let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
613        let mut layout_response =
614            state.layout_items(Some(bounds.size.width), bounds.size.height, &padding, cx);
615
616        // Only paint the visible items, if there is actually any space for them (taking padding into account)
617        if bounds.size.height > padding.top + padding.bottom {
618            // Paint the visible items
619            cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
620                let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
621                item_origin.y -= layout_response.scroll_top.offset_in_item;
622                for item_element in &mut layout_response.item_elements {
623                    let item_height = item_element
624                        .measure(layout_response.available_item_space, cx)
625                        .height;
626                    item_element.draw(item_origin, layout_response.available_item_space, cx);
627                    item_origin.y += item_height;
628                }
629            });
630        }
631
632        state.last_layout_bounds = Some(bounds);
633        state.last_padding = Some(padding);
634
635        let list_state = self.state.clone();
636        let height = bounds.size.height;
637
638        cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
639            if phase == DispatchPhase::Bubble
640                && bounds.contains(&event.position)
641                && cx.was_top_layer(&event.position, cx.stacking_order())
642            {
643                list_state.0.borrow_mut().scroll(
644                    &layout_response.scroll_top,
645                    height,
646                    event.delta.pixel_delta(px(20.)),
647                    cx,
648                    padding,
649                )
650            }
651        });
652    }
653}
654
655impl IntoElement for List {
656    type Element = Self;
657
658    fn element_id(&self) -> Option<crate::ElementId> {
659        None
660    }
661
662    fn into_element(self) -> Self::Element {
663        self
664    }
665}
666
667impl Styled for List {
668    fn style(&mut self) -> &mut StyleRefinement {
669        &mut self.style
670    }
671}
672
673impl sum_tree::Item for ListItem {
674    type Summary = ListItemSummary;
675
676    fn summary(&self) -> Self::Summary {
677        match self {
678            ListItem::Unrendered => ListItemSummary {
679                count: 1,
680                rendered_count: 0,
681                unrendered_count: 1,
682                height: px(0.),
683            },
684            ListItem::Rendered { size } => ListItemSummary {
685                count: 1,
686                rendered_count: 1,
687                unrendered_count: 0,
688                height: size.height,
689            },
690        }
691    }
692}
693
694impl sum_tree::Summary for ListItemSummary {
695    type Context = ();
696
697    fn add_summary(&mut self, summary: &Self, _: &()) {
698        self.count += summary.count;
699        self.rendered_count += summary.rendered_count;
700        self.unrendered_count += summary.unrendered_count;
701        self.height += summary.height;
702    }
703}
704
705impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
706    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
707        self.0 += summary.count;
708    }
709}
710
711impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
712    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
713        self.0 += summary.rendered_count;
714    }
715}
716
717impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
718    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
719        self.0 += summary.unrendered_count;
720    }
721}
722
723impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
724    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
725        self.0 += summary.height;
726    }
727}
728
729impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
730    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
731        self.0.partial_cmp(&other.count).unwrap()
732    }
733}
734
735impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
736    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
737        self.0.partial_cmp(&other.height).unwrap()
738    }
739}
740
741#[cfg(test)]
742mod test {
743
744    use gpui::{ScrollDelta, ScrollWheelEvent};
745
746    use crate::{self as gpui, TestAppContext};
747
748    #[gpui::test]
749    fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
750        use crate::{div, list, point, px, size, Element, ListState, Styled};
751
752        let cx = cx.add_empty_window();
753
754        let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _| {
755            div().h(px(10.)).w_full().into_any()
756        });
757
758        // Ensure that the list is scrolled to the top
759        state.scroll_to(gpui::ListOffset {
760            item_ix: 0,
761            offset_in_item: px(0.0),
762        });
763
764        // Paint
765        cx.draw(
766            point(px(0.), px(0.)),
767            size(px(100.), px(20.)).into(),
768            |_| list(state.clone()).w_full().h_full().z_index(10).into_any(),
769        );
770
771        // Reset
772        state.reset(5);
773
774        // And then receive a scroll event _before_ the next paint
775        cx.simulate_event(ScrollWheelEvent {
776            position: point(px(1.), px(1.)),
777            delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
778            ..Default::default()
779        });
780
781        // Scroll position should stay at the top of the list
782        assert_eq!(state.logical_scroll_top().item_ix, 0);
783        assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
784    }
785}