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}