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