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