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