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}