1use crate::{
11 AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
12 FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,
13 Overflow, Pixels, Point, ScrollDelta, ScrollEvent, Size, Style, StyleRefinement, Styled,
14 Window, point, px, size,
15};
16use collections::VecDeque;
17use refineable::Refineable as _;
18use std::{cell::RefCell, ops::Range, rc::Rc};
19use sum_tree::{Bias, Dimensions, SumTree};
20
21type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static;
22
23pub fn list(
25 state: ListState,
26 render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static,
27) -> List {
28 List {
29 state,
30 render_item: Box::new(render_item),
31 style: StyleRefinement::default(),
32 sizing_behavior: ListSizingBehavior::default(),
33 }
34}
35
36pub struct List {
38 state: ListState,
39 render_item: Box<RenderItemFn>,
40 style: StyleRefinement,
41 sizing_behavior: ListSizingBehavior,
42}
43
44impl List {
45 pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
47 self.sizing_behavior = behavior;
48 self
49 }
50}
51
52#[derive(Clone)]
54pub struct ListState(Rc<RefCell<StateInner>>);
55
56impl std::fmt::Debug for ListState {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.write_str("ListState")
59 }
60}
61
62struct StateInner {
63 last_layout_bounds: Option<Bounds<Pixels>>,
64 last_padding: Option<Edges<Pixels>>,
65 items: SumTree<ListItem>,
66 scroll_anchor: Option<ScrollAnchor>,
67 alignment: ListAlignment,
68 overdraw: Pixels,
69 reset: bool,
70 #[allow(clippy::type_complexity)]
71 scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,
72 scrollbar_drag_start_height: Option<Pixels>,
73 measuring_behavior: ListMeasuringBehavior,
74 item_measurement_ready: Option<Box<dyn Fn(usize, &App) -> bool>>,
75 follow_state: FollowState,
76}
77
78#[derive(Clone, Copy)]
79struct ScrollAnchor {
80 position: ListOffset,
81 pending: Option<PendingScroll>,
82 provisional: bool,
83 pending_end: bool,
84}
85
86#[derive(Clone, Copy)]
87enum PendingScroll {
88 Absolute(Pixels),
89 Proportional(f32),
90}
91
92enum ScrollAnchorMode {
93 Absolute,
94 Proportional,
95}
96
97#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
99pub enum FollowMode {
100 #[default]
102 Normal,
103 Tail,
105}
106
107#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
108enum FollowState {
109 #[default]
110 Normal,
111 Tail {
112 is_following: bool,
113 },
114}
115
116impl FollowState {
117 fn is_following(&self) -> bool {
118 matches!(self, FollowState::Tail { is_following: true })
119 }
120
121 fn has_stopped_following(&self) -> bool {
122 matches!(
123 self,
124 FollowState::Tail {
125 is_following: false
126 }
127 )
128 }
129
130 fn start_following(&mut self) {
131 if let FollowState::Tail {
132 is_following: false,
133 } = self
134 {
135 *self = FollowState::Tail { is_following: true };
136 }
137 }
138
139 fn stop_following(&mut self) {
140 if let FollowState::Tail { is_following: true } = self {
141 *self = FollowState::Tail {
142 is_following: false,
143 };
144 }
145 }
146}
147
148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150pub enum ListAlignment {
151 Top,
153 Bottom,
155}
156
157pub struct ListScrollEvent {
159 pub visible_range: Range<usize>,
161
162 pub count: usize,
164
165 pub is_scrolled: bool,
167
168 pub is_following_tail: bool,
170}
171
172#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
174pub enum ListSizingBehavior {
175 Infer,
177 #[default]
179 Auto,
180}
181
182#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
184pub enum ListMeasuringBehavior {
185 Measure(bool),
188 #[default]
190 Visible,
191}
192
193impl ListMeasuringBehavior {
194 fn reset(&mut self) {
195 match self {
196 ListMeasuringBehavior::Measure(has_measured) => *has_measured = false,
197 ListMeasuringBehavior::Visible => {}
198 }
199 }
200}
201
202#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
204pub enum ListHorizontalSizingBehavior {
205 #[default]
207 FitList,
208 Unconstrained,
210}
211
212struct LayoutItemsResponse {
213 max_item_width: Pixels,
214 scroll_top: ListOffset,
215 item_layouts: VecDeque<ItemLayout>,
216}
217
218struct ItemLayout {
219 index: usize,
220 element: AnyElement,
221 size: Size<Pixels>,
222}
223
224pub struct ListPrepaintState {
226 hitbox: Hitbox,
227 layout: LayoutItemsResponse,
228}
229
230#[derive(Clone)]
231enum ListItem {
232 Unmeasured {
233 size_hint: Option<Size<Pixels>>,
234 focus_handle: Option<FocusHandle>,
235 },
236 Measured {
237 size: Size<Pixels>,
238 focus_handle: Option<FocusHandle>,
239 },
240}
241
242impl ListItem {
243 fn with_measurement(&self, size: Size<Pixels>, ready: bool) -> Self {
244 if ready {
245 Self::Measured {
246 size,
247 focus_handle: self.focus_handle(),
248 }
249 } else {
250 Self::Unmeasured {
251 size_hint: Some(size),
252 focus_handle: self.focus_handle(),
253 }
254 }
255 }
256
257 fn size(&self) -> Option<Size<Pixels>> {
258 if let ListItem::Measured { size, .. } = self {
259 Some(*size)
260 } else {
261 None
262 }
263 }
264
265 fn size_hint(&self) -> Option<Size<Pixels>> {
266 match self {
267 ListItem::Measured { size, .. } => Some(*size),
268 ListItem::Unmeasured { size_hint, .. } => *size_hint,
269 }
270 }
271
272 fn focus_handle(&self) -> Option<FocusHandle> {
273 match self {
274 ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => {
275 focus_handle.clone()
276 }
277 }
278 }
279
280 fn contains_focused(&self, window: &Window, cx: &App) -> bool {
281 match self {
282 ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => {
283 focus_handle
284 .as_ref()
285 .is_some_and(|handle| handle.contains_focused(window, cx))
286 }
287 }
288 }
289}
290
291#[derive(Clone, Debug, Default, PartialEq)]
292struct ListItemSummary {
293 count: usize,
294 rendered_count: usize,
295 unrendered_count: usize,
296 height: Pixels,
297 has_focus_handles: bool,
298 has_unknown_height: bool,
299}
300
301#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
302struct Count(usize);
303
304#[derive(Clone, Debug, Default)]
305struct Height(Pixels);
306
307impl ListState {
308 pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self {
315 let state = Self(Rc::new(RefCell::new(StateInner {
316 last_layout_bounds: None,
317 last_padding: None,
318 items: SumTree::default(),
319 scroll_anchor: None,
320 alignment,
321 overdraw,
322 scroll_handler: None,
323 reset: false,
324 scrollbar_drag_start_height: None,
325 measuring_behavior: ListMeasuringBehavior::default(),
326 item_measurement_ready: None,
327 follow_state: FollowState::default(),
328 })));
329 state.splice(0..0, item_count);
330 state
331 }
332
333 pub fn measure_all(self) -> Self {
337 self.0.borrow_mut().measuring_behavior = ListMeasuringBehavior::Measure(false);
338 self
339 }
340
341 pub fn with_uniform_item_height(self, height: Pixels) -> Self {
348 self.apply_uniform_item_height(height);
349 self
350 }
351
352 pub fn reset(&self, element_count: usize) {
356 let old_count = {
357 let state = &mut *self.0.borrow_mut();
358 state.reset = true;
359 state.measuring_behavior.reset();
360 state.set_scroll_anchor(None);
361 state.scrollbar_drag_start_height = None;
362 state.items.summary().count
363 };
364
365 self.splice(0..old_count, element_count);
366 }
367
368 pub fn reset_with_uniform_height(&self, element_count: usize, height: Pixels) {
372 self.reset(element_count);
373 self.apply_uniform_item_height(height);
374 }
375
376 fn apply_uniform_item_height(&self, height: Pixels) {
377 let size_hint = Size {
378 width: px(0.),
379 height,
380 };
381 let mut state = self.0.borrow_mut();
382 let new_items = state
383 .items
384 .iter()
385 .map(|item| ListItem::Unmeasured {
386 size_hint: Some(item.size_hint().unwrap_or(size_hint)),
387 focus_handle: item.focus_handle(),
388 })
389 .collect::<Vec<_>>();
390 let mut tree = SumTree::default();
391 tree.extend(new_items, ());
392 state.items = tree;
393 }
394
395 pub fn remeasure(&self) {
400 let count = self.item_count();
401 self.remeasure_items_with_scroll_anchor(0..count, ScrollAnchorMode::Proportional);
402 }
403
404 pub fn remeasure_items(&self, range: Range<usize>) {
412 self.remeasure_items_with_scroll_anchor(range, ScrollAnchorMode::Absolute);
413 }
414
415 fn remeasure_items_with_scroll_anchor(&self, range: Range<usize>, mode: ScrollAnchorMode) {
416 let state = &mut *self.0.borrow_mut();
417
418 if let Some(anchor) = state.scroll_anchor.as_mut()
419 && range.contains(&anchor.position.item_ix)
420 {
421 anchor.pending = match mode {
422 ScrollAnchorMode::Absolute => {
423 Some(PendingScroll::Absolute(anchor.position.offset_in_item))
424 }
425 ScrollAnchorMode::Proportional => {
426 let mut cursor = state.items.cursor::<Count>(());
427 cursor.seek(&Count(anchor.position.item_ix), Bias::Right);
428 cursor
429 .item()
430 .and_then(|item| item.size())
431 .map(|size| {
432 let fraction = if size.height > px(0.) {
433 (anchor.position.offset_in_item / size.height).clamp(0., 1.)
434 } else {
435 0.
436 };
437 PendingScroll::Proportional(fraction)
438 })
439 .or(anchor.pending)
440 }
441 };
442 }
443
444 let new_items = {
447 let mut cursor = state.items.cursor::<Count>(());
448 let mut new_items = cursor.slice(&Count(range.start), Bias::Right);
449 let invalidated = cursor.slice(&Count(range.end), Bias::Right);
450 new_items.extend(
451 invalidated.iter().map(|item| ListItem::Unmeasured {
452 size_hint: item.size_hint(),
453 focus_handle: item.focus_handle(),
454 }),
455 (),
456 );
457 new_items.append(cursor.suffix(), ());
458 new_items
459 };
460 state.items = new_items;
461 state.measuring_behavior.reset();
462 }
463
464 pub fn item_count(&self) -> usize {
466 self.0.borrow().items.summary().count
467 }
468
469 pub fn is_scrolled_to_end(&self) -> Option<bool> {
472 let state = self.0.borrow();
473 let bounds = state.last_layout_bounds?;
474 let summary = state.items.summary();
475 if summary.has_unknown_height {
476 return None;
477 }
478 let padding = state.last_padding.unwrap_or_default();
479 let content_height = summary.height + padding.top + padding.bottom;
480 let scroll_max = (content_height - bounds.size.height).max(px(0.));
481 if scroll_max <= px(0.) {
482 return None;
483 }
484 let scroll_top = state.scroll_top(&state.logical_scroll_top());
485 Some(scroll_top >= scroll_max)
486 }
487
488 pub fn splice(&self, old_range: Range<usize>, count: usize) {
491 self.splice_focusable(old_range, (0..count).map(|_| None))
492 }
493
494 pub fn splice_focusable(
499 &self,
500 old_range: Range<usize>,
501 focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
502 ) {
503 let state = &mut *self.0.borrow_mut();
504
505 let mut old_items = state.items.cursor::<Count>(());
506 let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right);
507 old_items.seek_forward(&Count(old_range.end), Bias::Right);
508
509 let mut spliced_count = 0;
510 new_items.extend(
511 focus_handles.into_iter().map(|focus_handle| {
512 spliced_count += 1;
513 ListItem::Unmeasured {
514 size_hint: None,
515 focus_handle,
516 }
517 }),
518 (),
519 );
520 new_items.append(old_items.suffix(), ());
521 drop(old_items);
522 state.items = new_items;
523
524 if let Some(anchor) = state.scroll_anchor.as_mut() {
525 if anchor.provisional && old_range.start <= anchor.position.item_ix {
526 anchor.provisional = false;
527 anchor.pending = None;
528 }
529 if old_range.contains(&anchor.position.item_ix) {
530 anchor.position = ListOffset {
531 item_ix: old_range.start,
532 offset_in_item: px(0.),
533 };
534 anchor.pending = None;
535 } else if old_range.end <= anchor.position.item_ix {
536 anchor.position.item_ix = anchor.position.item_ix - old_range.len() + spliced_count;
537 }
538 }
539 }
540
541 pub fn set_scroll_handler(
543 &self,
544 handler: impl FnMut(&ListScrollEvent, &mut Window, &mut App) + 'static,
545 ) {
546 self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
547 }
548
549 pub fn logical_scroll_top(&self) -> ListOffset {
551 self.0.borrow().logical_scroll_top()
552 }
553
554 pub fn scroll_by(&self, distance: Pixels) {
556 if distance == px(0.) {
557 return;
558 }
559
560 let current_offset = self.logical_scroll_top();
561 let state = &mut *self.0.borrow_mut();
562
563 if distance < px(0.) {
564 state.follow_state.stop_following();
565 }
566
567 let mut cursor = state.items.cursor::<ListItemSummary>(());
568 cursor.seek(&Count(current_offset.item_ix), Bias::Right);
569
570 let start_pixel_offset = cursor.start().height + current_offset.offset_in_item;
571 let new_pixel_offset = (start_pixel_offset + distance).max(px(0.));
572 if new_pixel_offset > start_pixel_offset {
573 cursor.seek_forward(&Height(new_pixel_offset), Bias::Right);
574 } else {
575 cursor.seek(&Height(new_pixel_offset), Bias::Right);
576 }
577
578 let scroll_top = ListOffset {
579 item_ix: cursor.start().count,
580 offset_in_item: new_pixel_offset - cursor.start().height,
581 };
582 drop(cursor);
583 state.set_scroll_anchor(Some(scroll_top));
584 }
585
586 pub fn scroll_to_end(&self) {
593 let state = &mut *self.0.borrow_mut();
594 let item_count = state.items.summary().count;
595 state.set_scroll_anchor(Some(ListOffset {
596 item_ix: item_count,
597 offset_in_item: px(0.),
598 }));
599 }
600
601 pub fn set_follow_mode(&self, mode: FollowMode) {
606 if mode == FollowMode::Normal {
607 self.pause_following_tail();
608 }
609 let state = &mut *self.0.borrow_mut();
610
611 match mode {
612 FollowMode::Normal => {
613 state.follow_state = FollowState::Normal;
614 }
615 FollowMode::Tail => {
616 state.follow_state = FollowState::Tail { is_following: true };
617 if matches!(mode, FollowMode::Tail) {
618 let item_count = state.items.summary().count;
619 state.set_scroll_anchor(Some(ListOffset {
620 item_ix: item_count,
621 offset_in_item: px(0.),
622 }));
623 }
624 }
625 }
626 }
627
628 pub fn pause_following_tail(&self) {
638 let state = &mut *self.0.borrow_mut();
639 if state.follow_state.is_following()
640 && let Some(anchor) = state.scroll_anchor.as_mut()
641 {
642 anchor.pending_end = false;
643 }
644 state.follow_state.stop_following();
645 }
646
647 pub fn set_item_measurement_readiness(&self, ready: impl Fn(usize, &App) -> bool + 'static) {
651 self.0.borrow_mut().item_measurement_ready = Some(Box::new(ready));
652 }
653
654 pub fn is_following_tail(&self) -> bool {
657 matches!(
658 self.0.borrow().follow_state,
659 FollowState::Tail { is_following: true }
660 )
661 }
662
663 pub fn scroll_to(&self, mut scroll_top: ListOffset) {
665 let state = &mut *self.0.borrow_mut();
666 let item_count = state.items.summary().count;
667 if scroll_top.item_ix >= item_count {
668 scroll_top.item_ix = item_count;
669 scroll_top.offset_in_item = px(0.);
670 }
671
672 if scroll_top.item_ix < item_count {
673 state.follow_state.stop_following();
674 }
675
676 state.set_scroll_anchor(Some(scroll_top));
677 }
678
679 pub fn scroll_to_reveal_item(&self, ix: usize) {
681 let state = &mut *self.0.borrow_mut();
682
683 let mut scroll_top = state.logical_scroll_top();
684 let height = state
685 .last_layout_bounds
686 .map_or(px(0.), |bounds| bounds.size.height);
687 let padding = state.last_padding.unwrap_or_default();
688
689 if ix <= scroll_top.item_ix {
690 scroll_top.item_ix = ix;
691 scroll_top.offset_in_item = px(0.);
692 } else {
693 let mut cursor = state.items.cursor::<ListItemSummary>(());
694 cursor.seek(&Count(ix + 1), Bias::Right);
695 let bottom = cursor.start().height + padding.top;
696 let goal_top = px(0.).max(bottom - height + padding.bottom);
697
698 cursor.seek(&Height(goal_top), Bias::Left);
699 let start_ix = cursor.start().count;
700 let start_item_top = cursor.start().height;
701
702 if start_ix >= scroll_top.item_ix {
703 scroll_top.item_ix = start_ix;
704 scroll_top.offset_in_item = goal_top - start_item_top;
705 }
706 }
707
708 state.set_scroll_anchor(Some(scroll_top));
709 }
710
711 pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
714 let state = &*self.0.borrow();
715
716 let bounds = state.last_layout_bounds.unwrap_or_default();
717 let scroll_top = state.logical_scroll_top();
718 if ix < scroll_top.item_ix {
719 return None;
720 }
721
722 let mut cursor = state.items.cursor::<Dimensions<Count, Height>>(());
723 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
724
725 let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item;
726
727 cursor.seek_forward(&Count(ix), Bias::Right);
728 if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
729 let &Dimensions(Count(count), Height(top), _) = cursor.start();
730 if count == ix {
731 let top = bounds.top() + top - scroll_top;
732 return Some(Bounds::from_corners(
733 point(bounds.left(), top),
734 point(bounds.right(), top + size.height),
735 ));
736 }
737 }
738 None
739 }
740
741 pub fn scrollbar_drag_started(&self) {
746 let mut state = self.0.borrow_mut();
747 state.scrollbar_drag_start_height = Some(state.items.summary().height);
748 }
749
750 pub fn scrollbar_drag_ended(&self) {
754 self.0.borrow_mut().scrollbar_drag_start_height.take();
755 }
756
757 pub fn is_scrollbar_dragging(&self) -> bool {
764 self.0.borrow().scrollbar_drag_start_height.is_some()
765 }
766
767 pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
769 self.0.borrow_mut().set_offset_from_scrollbar(point);
770 }
771
772 pub fn max_offset_for_scrollbar(&self) -> Point<Pixels> {
775 let state = self.0.borrow();
776 point(Pixels::ZERO, state.max_scroll_offset())
777 }
778
779 pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
784 let state = &self.0.borrow();
785
786 if state.scroll_anchor.is_none() && state.alignment == ListAlignment::Bottom {
787 return Point::new(px(0.), -state.max_scroll_offset());
788 }
789
790 let logical_scroll_top = state.logical_scroll_top();
791
792 let mut cursor = state.items.cursor::<ListItemSummary>(());
793 let summary: ListItemSummary =
794 cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right);
795 let offset = summary.height + logical_scroll_top.offset_in_item;
796
797 Point::new(px(0.), -offset)
798 }
799
800 pub fn viewport_bounds(&self) -> Bounds<Pixels> {
802 self.0.borrow().last_layout_bounds.unwrap_or_default()
803 }
804
805 pub fn item_is_above_viewport(&self, ix: usize) -> Option<bool> {
813 let viewport_bounds = self.0.borrow().last_layout_bounds?;
814
815 let scroll_top = self.logical_scroll_top();
816 if ix < scroll_top.item_ix {
817 return Some(true);
820 }
821
822 let item_bounds = self.bounds_for_item(ix)?;
823 Some(item_bounds.bottom() <= viewport_bounds.top())
824 }
825
826 pub fn item_is_below_viewport(&self, ix: usize) -> Option<bool> {
832 let viewport_bounds = self.0.borrow().last_layout_bounds?;
833
834 let scroll_top = self.logical_scroll_top();
835 if ix < scroll_top.item_ix {
836 return Some(false);
839 }
840
841 let item_bounds = self.bounds_for_item(ix)?;
842 Some(item_bounds.top() >= viewport_bounds.bottom())
843 }
844}
845
846impl StateInner {
847 fn max_scroll_offset(&self) -> Pixels {
848 let bounds = self.last_layout_bounds.unwrap_or_default();
849 let height = self
850 .scrollbar_drag_start_height
851 .unwrap_or_else(|| self.items.summary().height);
852 (height - bounds.size.height).max(px(0.))
853 }
854
855 fn visible_range(
856 items: &SumTree<ListItem>,
857 height: Pixels,
858 scroll_top: &ListOffset,
859 ) -> Range<usize> {
860 let mut cursor = items.cursor::<ListItemSummary>(());
861 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
862 let start_y = cursor.start().height + scroll_top.offset_in_item;
863 cursor.seek_forward(&Height(start_y + height), Bias::Left);
864 scroll_top.item_ix..cursor.start().count + 1
865 }
866
867 fn scroll(
868 &mut self,
869 scroll_top: &ListOffset,
870 height: Pixels,
871 delta: Point<Pixels>,
872 current_view: EntityId,
873 window: &mut Window,
874 cx: &mut App,
875 ) {
876 if self.reset || delta.y == px(0.) {
879 return;
880 }
881
882 let padding = self.last_padding.unwrap_or_default();
883 let scroll_max =
884 (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
885 let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
886 .max(px(0.))
887 .min(scroll_max);
888
889 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
890 self.set_scroll_anchor(None);
891 } else {
892 let (start, ..) =
893 self.items
894 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
895 let scroll_top = ListOffset {
896 item_ix: start.count,
897 offset_in_item: new_scroll_top - start.height,
898 };
899 self.set_scroll_anchor(Some(scroll_top));
900 }
901
902 if delta.y > px(0.) {
903 self.follow_state.stop_following();
904 }
905
906 if let Some(handler) = self.scroll_handler.as_mut() {
907 let visible_range = Self::visible_range(&self.items, height, scroll_top);
908 handler(
909 &ListScrollEvent {
910 visible_range,
911 count: self.items.summary().count,
912 is_scrolled: self.scroll_anchor.is_some(),
913 is_following_tail: matches!(
914 self.follow_state,
915 FollowState::Tail { is_following: true }
916 ),
917 },
918 window,
919 cx,
920 );
921 }
922
923 cx.notify(current_view);
924 }
925
926 fn logical_scroll_top(&self) -> ListOffset {
927 self.scroll_anchor
928 .map(|anchor| anchor.position)
929 .unwrap_or_else(|| match self.alignment {
930 ListAlignment::Top => ListOffset {
931 item_ix: 0,
932 offset_in_item: px(0.),
933 },
934 ListAlignment::Bottom => ListOffset {
935 item_ix: self.items.summary().count,
936 offset_in_item: px(0.),
937 },
938 })
939 }
940
941 fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
942 let (start, ..) = self.items.find::<ListItemSummary, _>(
943 (),
944 &Count(logical_scroll_top.item_ix),
945 Bias::Right,
946 );
947 start.height + logical_scroll_top.offset_in_item
948 }
949
950 fn is_item_ready(&self, index: usize, cx: &App) -> bool {
951 self.item_measurement_ready
952 .as_ref()
953 .is_none_or(|ready| ready(index, cx))
954 }
955
956 fn is_provisional(&self) -> bool {
957 self.scroll_anchor.is_some_and(|anchor| anchor.provisional)
958 }
959
960 fn set_scroll_anchor(&mut self, position: Option<ListOffset>) {
961 let Some(position) = position else {
962 self.scroll_anchor = None;
963 return;
964 };
965 let pending = self
966 .scroll_anchor
967 .and_then(|anchor| anchor.pending)
968 .filter(|_| position.item_ix < self.items.summary().count)
969 .and_then(|pending| match (pending, self.is_provisional()) {
970 (PendingScroll::Absolute(_), _) | (_, true) => {
971 Some(PendingScroll::Absolute(position.offset_in_item))
972 }
973 (PendingScroll::Proportional(_), false) => {
974 let mut cursor = self.items.cursor::<Count>(());
975 cursor.seek(&Count(position.item_ix), Bias::Right);
976 cursor
977 .item()
978 .and_then(|item| item.size_hint())
979 .filter(|size| size.height > px(0.))
980 .map(|size| {
981 PendingScroll::Proportional(
982 (position.offset_in_item / size.height).clamp(0., 1.),
983 )
984 })
985 }
986 });
987 self.scroll_anchor = Some(ScrollAnchor {
988 position,
989 pending,
990 provisional: false,
991 pending_end: false,
992 });
993 }
994
995 fn layout_all_items(
996 &mut self,
997 available_width: Pixels,
998 render_item: &mut RenderItemFn,
999 window: &mut Window,
1000 cx: &mut App,
1001 ) {
1002 match &mut self.measuring_behavior {
1003 ListMeasuringBehavior::Visible => {
1004 return;
1005 }
1006 ListMeasuringBehavior::Measure(has_measured) => {
1007 if *has_measured {
1008 return;
1009 }
1010 *has_measured = true;
1011 }
1012 }
1013
1014 let mut cursor = self.items.cursor::<Count>(());
1015 let available_item_space = size(
1016 AvailableSpace::Definite(available_width),
1017 AvailableSpace::MinContent,
1018 );
1019
1020 let mut measured_items = Vec::default();
1021
1022 for (ix, item) in cursor.enumerate() {
1023 let size = item.size().unwrap_or_else(|| {
1024 let mut element = render_item(ix, window, cx);
1025 element.layout_as_root(available_item_space, window, cx)
1026 });
1027
1028 measured_items.push(item.with_measurement(size, self.is_item_ready(ix, cx)));
1029 }
1030
1031 self.items = SumTree::from_iter(measured_items, ());
1032 self.measuring_behavior =
1033 ListMeasuringBehavior::Measure(self.items.summary().unrendered_count == 0);
1034 }
1035
1036 fn layout_items(
1037 &mut self,
1038 available_width: Option<Pixels>,
1039 available_height: Pixels,
1040 padding: &Edges<Pixels>,
1041 render_item: &mut RenderItemFn,
1042 window: &mut Window,
1043 cx: &mut App,
1044 ) -> LayoutItemsResponse {
1045 let old_items = self.items.clone();
1046 let mut measured_items = VecDeque::new();
1047 let mut item_layouts = VecDeque::new();
1048 let mut rendered_height = padding.top;
1049 let mut max_item_width = px(0.);
1050 let mut scroll_top = self.logical_scroll_top();
1051
1052 if self.follow_state.is_following()
1053 || self.scroll_anchor.is_some_and(|anchor| anchor.pending_end)
1054 {
1055 scroll_top = ListOffset {
1056 item_ix: self.items.summary().count,
1057 offset_in_item: px(0.),
1058 };
1059 self.set_scroll_anchor(Some(scroll_top));
1060 }
1061
1062 let preserve_anchor = scroll_top.item_ix < self.items.summary().count;
1063 if !preserve_anchor {
1064 self.follow_state.start_following();
1065 }
1066 let mut provisional = false;
1067 let mut rendered_focused_item = false;
1068
1069 let available_item_space = size(
1070 available_width.map_or(AvailableSpace::MaxContent, |width| {
1071 AvailableSpace::Definite(width)
1072 }),
1073 AvailableSpace::MinContent,
1074 );
1075
1076 let mut cursor = old_items.cursor::<Count>(());
1077
1078 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
1080 for (ix, item) in cursor.by_ref().enumerate() {
1081 let visible_height = rendered_height - scroll_top.offset_in_item;
1082 if visible_height >= available_height + self.overdraw {
1083 break;
1084 }
1085
1086 let item_index = scroll_top.item_ix + ix;
1087 let (size, ready) = if visible_height >= available_height
1088 && let Some(size) = item.size()
1089 {
1090 (size, self.is_item_ready(item_index, cx))
1091 } else {
1092 let mut element = render_item(item_index, window, cx);
1093 let element_size = element.layout_as_root(available_item_space, window, cx);
1094 let ready = self.is_item_ready(item_index, cx);
1095 if ix == 0
1096 && ready
1097 && let Some(anchor) = self.scroll_anchor.as_mut()
1098 && let Some(pending) = anchor.pending.take()
1099 {
1100 scroll_top.offset_in_item = match pending {
1101 PendingScroll::Absolute(offset) => offset.min(element_size.height),
1102 PendingScroll::Proportional(fraction) => element_size.height * fraction,
1103 };
1104 anchor.position = scroll_top;
1105 }
1106
1107 if visible_height < available_height {
1108 item_layouts.push_back(ItemLayout {
1109 index: item_index,
1110 element,
1111 size: element_size,
1112 });
1113 if item.contains_focused(window, cx) {
1114 rendered_focused_item = true;
1115 }
1116 }
1117 (element_size, ready)
1118 };
1119
1120 rendered_height += size.height;
1121 max_item_width = max_item_width.max(size.width);
1122 provisional |= !ready;
1123 measured_items.push_back(item.with_measurement(size, ready));
1124 if !ready && visible_height >= available_height {
1125 break;
1126 }
1127 }
1128 rendered_height += padding.bottom;
1129
1130 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
1132
1133 if !provisional && rendered_height - scroll_top.offset_in_item < available_height {
1136 let forward_layout_count = item_layouts.len();
1137 let forward_focused = rendered_focused_item;
1138 while rendered_height < available_height {
1139 cursor.prev();
1140 if let Some(item) = cursor.item() {
1141 let item_index = cursor.start().0;
1142 let mut element = render_item(item_index, window, cx);
1143 let element_size = element.layout_as_root(available_item_space, window, cx);
1144 let ready = self.is_item_ready(item_index, cx);
1145 provisional |= !ready;
1146 rendered_height += element_size.height;
1147 measured_items.push_front(item.with_measurement(element_size, ready));
1148 item_layouts.push_front(ItemLayout {
1149 index: item_index,
1150 element,
1151 size: element_size,
1152 });
1153 if item.contains_focused(window, cx) {
1154 rendered_focused_item = true;
1155 }
1156 if preserve_anchor && provisional {
1157 break;
1158 }
1159 } else {
1160 break;
1161 }
1162 }
1163
1164 if preserve_anchor && provisional {
1165 item_layouts.drain(..item_layouts.len() - forward_layout_count);
1166 rendered_focused_item = forward_focused;
1167 } else {
1168 scroll_top = ListOffset {
1169 item_ix: cursor.start().0,
1170 offset_in_item: rendered_height - available_height,
1171 };
1172
1173 match self.alignment {
1174 ListAlignment::Top => {
1175 scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
1176 self.set_scroll_anchor(Some(scroll_top));
1177 }
1178 ListAlignment::Bottom => {
1179 scroll_top = ListOffset {
1180 item_ix: cursor.start().0,
1181 offset_in_item: rendered_height - available_height,
1182 };
1183 self.set_scroll_anchor(None);
1184 }
1185 };
1186 }
1187 }
1188
1189 if let Some(anchor) = self.scroll_anchor.as_mut() {
1190 anchor.provisional = preserve_anchor && provisional;
1191 anchor.pending_end = !preserve_anchor && provisional;
1192 }
1193
1194 if !provisional {
1196 let mut leading_overdraw = scroll_top.offset_in_item;
1197 while leading_overdraw < self.overdraw {
1198 cursor.prev();
1199 if let Some(item) = cursor.item() {
1200 let size = if let ListItem::Measured { size, .. } = item {
1201 *size
1202 } else {
1203 let mut element = render_item(cursor.start().0, window, cx);
1204 element.layout_as_root(available_item_space, window, cx)
1205 };
1206
1207 leading_overdraw += size.height;
1208 let ready = self.is_item_ready(cursor.start().0, cx);
1209 measured_items.push_front(item.with_measurement(size, ready));
1210 if !ready {
1211 break;
1212 }
1213 } else {
1214 break;
1215 }
1216 }
1217 }
1218
1219 let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
1220 let mut cursor = old_items.cursor::<Count>(());
1221 let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right);
1222 new_items.extend(measured_items, ());
1223 cursor.seek(&Count(measured_range.end), Bias::Right);
1224 new_items.append(cursor.suffix(), ());
1225 self.items = new_items;
1226
1227 if !provisional && self.follow_state.has_stopped_following() {
1231 let padding = self.last_padding.unwrap_or_default();
1232 let total_height = self.items.summary().height + padding.top + padding.bottom;
1233 let scroll_offset = self.scroll_top(&scroll_top);
1234 if scroll_offset + available_height >= total_height - px(1.0) {
1235 self.follow_state.start_following();
1236 }
1237 }
1238
1239 if !rendered_focused_item {
1243 let mut cursor = self
1244 .items
1245 .filter::<_, Count>((), |summary| summary.has_focus_handles);
1246 cursor.next();
1247 while let Some(item) = cursor.item() {
1248 if item.contains_focused(window, cx) {
1249 let item_index = cursor.start().0;
1250 let mut element = render_item(cursor.start().0, window, cx);
1251 let size = element.layout_as_root(available_item_space, window, cx);
1252 item_layouts.push_back(ItemLayout {
1253 index: item_index,
1254 element,
1255 size,
1256 });
1257 break;
1258 }
1259 cursor.next();
1260 }
1261 }
1262
1263 LayoutItemsResponse {
1264 max_item_width,
1265 scroll_top,
1266 item_layouts,
1267 }
1268 }
1269
1270 fn prepaint_items(
1271 &mut self,
1272 bounds: Bounds<Pixels>,
1273 padding: Edges<Pixels>,
1274 autoscroll: bool,
1275 render_item: &mut RenderItemFn,
1276 window: &mut Window,
1277 cx: &mut App,
1278 ) -> Result<LayoutItemsResponse, ListOffset> {
1279 window.transact(|window| {
1280 match self.measuring_behavior {
1281 ListMeasuringBehavior::Measure(has_measured) if !has_measured => {
1282 self.layout_all_items(bounds.size.width, render_item, window, cx);
1283 }
1284 _ => {}
1285 }
1286
1287 let mut layout_response = self.layout_items(
1288 Some(bounds.size.width),
1289 bounds.size.height,
1290 &padding,
1291 render_item,
1292 window,
1293 cx,
1294 );
1295
1296 window.take_autoscroll();
1298
1299 if bounds.size.height > padding.top + padding.bottom {
1301 let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
1302 item_origin.y -= layout_response.scroll_top.offset_in_item;
1303 for item in &mut layout_response.item_layouts {
1304 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1305 item.element.prepaint_at(item_origin, window, cx);
1306 });
1307
1308 if let Some(autoscroll_bounds) = window.take_autoscroll()
1309 && autoscroll
1310 {
1311 if autoscroll_bounds.top() < bounds.top() {
1312 let mut item_ix = item.index;
1313 let mut offset_in_item = autoscroll_bounds.top() - item_origin.y;
1314
1315 if offset_in_item < Pixels::ZERO {
1320 let mut cursor = self.items.cursor::<Count>(());
1321 cursor.seek(&Count(item_ix), Bias::Right);
1322 while offset_in_item < Pixels::ZERO {
1323 cursor.prev();
1324 let Some(prev_item) = cursor.item() else {
1325 offset_in_item = Pixels::ZERO;
1326 break;
1327 };
1328 let size = prev_item.size().unwrap_or_else(|| {
1329 let mut element = render_item(cursor.start().0, window, cx);
1330 let item_available_size = size(
1331 bounds.size.width.into(),
1332 AvailableSpace::MinContent,
1333 );
1334 element.layout_as_root(item_available_size, window, cx)
1335 });
1336 item_ix = cursor.start().0;
1337 offset_in_item += size.height;
1338 }
1339 }
1340
1341 return Err(ListOffset {
1342 item_ix,
1343 offset_in_item,
1344 });
1345 } else if autoscroll_bounds.bottom() > bounds.bottom() {
1346 let mut cursor = self.items.cursor::<Count>(());
1347 cursor.seek(&Count(item.index), Bias::Right);
1348 let mut height = bounds.size.height - padding.top - padding.bottom;
1349
1350 height -= autoscroll_bounds.bottom() - item_origin.y;
1352
1353 while height > Pixels::ZERO {
1355 cursor.prev();
1356 let Some(item) = cursor.item() else { break };
1357
1358 let size = item.size().unwrap_or_else(|| {
1359 let mut item = render_item(cursor.start().0, window, cx);
1360 let item_available_size =
1361 size(bounds.size.width.into(), AvailableSpace::MinContent);
1362 item.layout_as_root(item_available_size, window, cx)
1363 });
1364 height -= size.height;
1365 }
1366
1367 return Err(ListOffset {
1368 item_ix: cursor.start().0,
1369 offset_in_item: if height < Pixels::ZERO {
1370 -height
1371 } else {
1372 Pixels::ZERO
1373 },
1374 });
1375 }
1376 }
1377
1378 item_origin.y += item.size.height;
1379 }
1380 } else {
1381 layout_response.item_layouts.clear();
1382 }
1383
1384 Ok(layout_response)
1385 })
1386 }
1387
1388 fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
1391 let Some(bounds) = self.last_layout_bounds else {
1392 return;
1393 };
1394 let height = bounds.size.height;
1395
1396 let padding = self.last_padding.unwrap_or_default();
1397 let content_height = self
1400 .scrollbar_drag_start_height
1401 .unwrap_or_else(|| self.items.summary().height);
1402 let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
1403 let new_scroll_top = (-point.y).max(px(0.)).min(scroll_max);
1404
1405 let dragged_to_end =
1408 scroll_max > px(0.) && new_scroll_top >= (scroll_max - px(1.0)).max(px(0.));
1409 if dragged_to_end && matches!(self.follow_state, FollowState::Tail { .. }) {
1410 self.follow_state = FollowState::Tail { is_following: true };
1411 let item_count = self.items.summary().count;
1412 self.set_scroll_anchor(Some(ListOffset {
1413 item_ix: item_count,
1414 offset_in_item: px(0.),
1415 }));
1416 return;
1417 }
1418
1419 self.follow_state.stop_following();
1420
1421 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
1422 self.set_scroll_anchor(None);
1423 } else {
1424 let (start, _, _) =
1425 self.items
1426 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
1427
1428 let scroll_top = ListOffset {
1429 item_ix: start.count,
1430 offset_in_item: new_scroll_top - start.height,
1431 };
1432 self.set_scroll_anchor(Some(scroll_top));
1433 }
1434 }
1435}
1436
1437impl std::fmt::Debug for ListItem {
1438 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1439 match self {
1440 Self::Unmeasured { .. } => write!(f, "Unrendered"),
1441 Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
1442 }
1443 }
1444}
1445
1446#[derive(Debug, Clone, Copy, Default, PartialEq)]
1449pub struct ListOffset {
1450 pub item_ix: usize,
1452 pub offset_in_item: Pixels,
1454}
1455
1456impl Element for List {
1457 type RequestLayoutState = ();
1458 type PrepaintState = ListPrepaintState;
1459
1460 fn id(&self) -> Option<crate::ElementId> {
1461 None
1462 }
1463
1464 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1465 None
1466 }
1467
1468 fn request_layout(
1469 &mut self,
1470 _id: Option<&GlobalElementId>,
1471 _inspector_id: Option<&InspectorElementId>,
1472 window: &mut Window,
1473 cx: &mut App,
1474 ) -> (crate::LayoutId, Self::RequestLayoutState) {
1475 let layout_id = match self.sizing_behavior {
1476 ListSizingBehavior::Infer => {
1477 let mut style = Style::default();
1478 style.overflow.y = Overflow::Scroll;
1479 style.refine(&self.style);
1480 window.with_text_style(style.text_style().cloned(), |window| {
1481 let state = &mut *self.state.0.borrow_mut();
1482
1483 let available_height = if let Some(last_bounds) = state.last_layout_bounds {
1484 last_bounds.size.height
1485 } else {
1486 state.overdraw
1489 };
1490 let padding = style.padding.to_pixels(
1491 state.last_layout_bounds.unwrap_or_default().size.into(),
1492 window.rem_size(),
1493 );
1494
1495 let layout_response = state.layout_items(
1496 None,
1497 available_height,
1498 &padding,
1499 &mut self.render_item,
1500 window,
1501 cx,
1502 );
1503 let max_element_width = layout_response.max_item_width;
1504
1505 let summary = state.items.summary();
1506 let total_height = summary.height;
1507
1508 window.request_measured_layout(
1509 style,
1510 move |known_dimensions, available_space, _window, _cx| {
1511 let width =
1512 known_dimensions
1513 .width
1514 .unwrap_or(match available_space.width {
1515 AvailableSpace::Definite(x) => x,
1516 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1517 max_element_width
1518 }
1519 });
1520 let height = match available_space.height {
1521 AvailableSpace::Definite(height) => total_height.min(height),
1522 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1523 total_height
1524 }
1525 };
1526 size(width, height)
1527 },
1528 )
1529 })
1530 }
1531 ListSizingBehavior::Auto => {
1532 let mut style = Style::default();
1533 style.refine(&self.style);
1534 window.with_text_style(style.text_style().cloned(), |window| {
1535 window.request_layout(style, None, cx)
1536 })
1537 }
1538 };
1539 (layout_id, ())
1540 }
1541
1542 fn prepaint(
1543 &mut self,
1544 _id: Option<&GlobalElementId>,
1545 _inspector_id: Option<&InspectorElementId>,
1546 bounds: Bounds<Pixels>,
1547 _: &mut Self::RequestLayoutState,
1548 window: &mut Window,
1549 cx: &mut App,
1550 ) -> ListPrepaintState {
1551 let state = &mut *self.state.0.borrow_mut();
1552 state.reset = false;
1553
1554 let mut style = Style::default();
1555 style.refine(&self.style);
1556
1557 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1558
1559 if state
1561 .last_layout_bounds
1562 .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width)
1563 {
1564 let new_items = SumTree::from_iter(
1565 state.items.iter().map(|item| ListItem::Unmeasured {
1566 size_hint: None,
1567 focus_handle: item.focus_handle(),
1568 }),
1569 (),
1570 );
1571
1572 state.items = new_items;
1573 state.measuring_behavior.reset();
1574 }
1575
1576 let padding = style
1577 .padding
1578 .to_pixels(bounds.size.into(), window.rem_size());
1579 let mut autoscroll = true;
1580 let layout = loop {
1581 match state.prepaint_items(
1582 bounds,
1583 padding,
1584 autoscroll,
1585 &mut self.render_item,
1586 window,
1587 cx,
1588 ) {
1589 Ok(layout) => break layout,
1590 Err(autoscroll_request) => {
1591 state.set_scroll_anchor(Some(autoscroll_request));
1592 autoscroll = false;
1593 }
1594 }
1595 };
1596
1597 state.last_layout_bounds = Some(bounds);
1598 state.last_padding = Some(padding);
1599 ListPrepaintState { hitbox, layout }
1600 }
1601
1602 fn paint(
1603 &mut self,
1604 _id: Option<&GlobalElementId>,
1605 _inspector_id: Option<&InspectorElementId>,
1606 bounds: Bounds<crate::Pixels>,
1607 _: &mut Self::RequestLayoutState,
1608 prepaint: &mut Self::PrepaintState,
1609 window: &mut Window,
1610 cx: &mut App,
1611 ) {
1612 let current_view = window.current_view();
1613
1614 let list_state = self.state.clone();
1620 let height = bounds.size.height;
1621 let scroll_top = prepaint.layout.scroll_top;
1622 let hitbox_id = prepaint.hitbox.id;
1623 let mut accumulated_scroll_delta = ScrollDelta::default();
1624 window.on_mouse_event(move |event: &ScrollEvent, phase, window, cx| {
1625 if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) {
1626 accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
1627 let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
1628 list_state.0.borrow_mut().scroll(
1629 &scroll_top,
1630 height,
1631 pixel_delta,
1632 current_view,
1633 window,
1634 cx,
1635 )
1636 }
1637 });
1638
1639 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1640 for item in &mut prepaint.layout.item_layouts {
1641 item.element.paint(window, cx);
1642 }
1643 });
1644 }
1645}
1646
1647impl IntoElement for List {
1648 type Element = Self;
1649
1650 fn into_element(self) -> Self::Element {
1651 self
1652 }
1653}
1654
1655impl Styled for List {
1656 fn style(&mut self) -> &mut StyleRefinement {
1657 &mut self.style
1658 }
1659}
1660
1661impl sum_tree::Item for ListItem {
1662 type Summary = ListItemSummary;
1663
1664 fn summary(&self, _: ()) -> Self::Summary {
1665 match self {
1666 ListItem::Unmeasured {
1667 size_hint,
1668 focus_handle,
1669 } => ListItemSummary {
1670 count: 1,
1671 rendered_count: 0,
1672 unrendered_count: 1,
1673 height: if let Some(size) = size_hint {
1674 size.height
1675 } else {
1676 px(0.)
1677 },
1678 has_focus_handles: focus_handle.is_some(),
1679 has_unknown_height: size_hint.is_none(),
1680 },
1681 ListItem::Measured {
1682 size, focus_handle, ..
1683 } => ListItemSummary {
1684 count: 1,
1685 rendered_count: 1,
1686 unrendered_count: 0,
1687 height: size.height,
1688 has_focus_handles: focus_handle.is_some(),
1689 has_unknown_height: false,
1690 },
1691 }
1692 }
1693}
1694
1695impl sum_tree::ContextLessSummary for ListItemSummary {
1696 fn zero() -> Self {
1697 Default::default()
1698 }
1699
1700 fn add_summary(&mut self, summary: &Self) {
1701 self.count += summary.count;
1702 self.rendered_count += summary.rendered_count;
1703 self.unrendered_count += summary.unrendered_count;
1704 self.height += summary.height;
1705 self.has_focus_handles |= summary.has_focus_handles;
1706 self.has_unknown_height |= summary.has_unknown_height;
1707 }
1708}
1709
1710impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
1711 fn zero(_cx: ()) -> Self {
1712 Default::default()
1713 }
1714
1715 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1716 self.0 += summary.count;
1717 }
1718}
1719
1720impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
1721 fn zero(_cx: ()) -> Self {
1722 Default::default()
1723 }
1724
1725 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1726 self.0 += summary.height;
1727 }
1728}
1729
1730impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Count {
1731 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1732 Ord::cmp(&self.0, &other.count)
1733 }
1734}
1735
1736impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height {
1737 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1738 Ord::cmp(&self.0, &other.height)
1739 }
1740}
1741
1742#[cfg(test)]
1743mod test {
1744
1745 use gpui::{ScrollDelta, ScrollEvent};
1746 use std::cell::{Cell, RefCell};
1747 use std::rc::Rc;
1748
1749 use crate::{
1750 self as gpui, AnyElement, AppContext, Bounds, Context, Element, FollowMode,
1751 InteractiveElement, IntoElement, ListOffset, ListState, Pixels, Render, Styled,
1752 TestAppContext, VisualTestContext, Window, canvas, div, list, point, px, size,
1753 };
1754
1755 #[gpui::test]
1756 fn test_autoscroll_above_item_top_renders_items_above(cx: &mut TestAppContext) {
1757 let cx = cx.add_empty_window();
1758
1759 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1760 state.scroll_to(gpui::ListOffset {
1761 item_ix: 2,
1762 offset_in_item: px(0.),
1763 });
1764
1765 struct TestView(ListState);
1766 impl Render for TestView {
1767 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1768 list(self.0.clone(), |ix, _, _| {
1769 if ix == 2 {
1770 canvas(
1773 |bounds, window, _| {
1774 window.request_autoscroll(Bounds::from_corners(
1775 point(bounds.left(), bounds.top() - px(30.)),
1776 point(bounds.right(), bounds.top() + px(5.)),
1777 ));
1778 },
1779 |_, _, _, _| {},
1780 )
1781 .h(px(20.))
1782 .w_full()
1783 .into_any()
1784 } else {
1785 div().h(px(20.)).w_full().into_any()
1786 }
1787 })
1788 .w_full()
1789 .h_full()
1790 }
1791 }
1792
1793 cx.draw(point(px(0.), px(0.)), size(px(100.), px(60.)), |_, cx| {
1794 cx.new(|_| TestView(state.clone())).into_any_element()
1795 });
1796
1797 let scroll_top = state.logical_scroll_top();
1799 assert!(
1800 scroll_top.offset_in_item >= px(0.),
1801 "offset_in_item must never be negative (would leave blank space above), got {:?}",
1802 scroll_top.offset_in_item,
1803 );
1804 assert_eq!(scroll_top.item_ix, 0);
1805 assert_eq!(scroll_top.offset_in_item, px(10.));
1806 }
1807
1808 #[gpui::test]
1809 fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
1810 let cx = cx.add_empty_window();
1811
1812 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1813
1814 state.scroll_to(gpui::ListOffset {
1816 item_ix: 0,
1817 offset_in_item: px(0.0),
1818 });
1819
1820 struct TestView(ListState);
1821 impl Render for TestView {
1822 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1823 list(self.0.clone(), |_, _, _| {
1824 div().h(px(10.)).w_full().into_any()
1825 })
1826 .w_full()
1827 .h_full()
1828 }
1829 }
1830
1831 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1833 cx.new(|_| TestView(state.clone())).into_any_element()
1834 });
1835
1836 state.reset(5);
1838
1839 cx.simulate_event(ScrollEvent {
1841 position: point(px(1.), px(1.)),
1842 delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
1843 ..Default::default()
1844 });
1845
1846 assert_eq!(state.logical_scroll_top().item_ix, 0);
1848 assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
1849 }
1850
1851 #[gpui::test]
1852 fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) {
1853 let cx = cx.add_empty_window();
1854
1855 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1856
1857 struct TestView(ListState);
1858 impl Render for TestView {
1859 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1860 list(self.0.clone(), |_, _, _| {
1861 div().h(px(20.)).w_full().into_any()
1862 })
1863 .w_full()
1864 .h_full()
1865 }
1866 }
1867
1868 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
1870 cx.new(|_| TestView(state.clone())).into_any_element()
1871 });
1872
1873 state.scroll_by(px(30.));
1875
1876 let offset = state.logical_scroll_top();
1878 assert_eq!(offset.item_ix, 1);
1879 assert_eq!(offset.offset_in_item, px(10.));
1880
1881 state.scroll_by(px(-30.));
1883
1884 let offset = state.logical_scroll_top();
1886 assert_eq!(offset.item_ix, 0);
1887 assert_eq!(offset.offset_in_item, px(0.));
1888
1889 state.scroll_by(px(0.));
1891 let offset = state.logical_scroll_top();
1892 assert_eq!(offset.item_ix, 0);
1893 assert_eq!(offset.offset_in_item, px(0.));
1894 }
1895
1896 #[gpui::test]
1897 fn test_child_scroll_handler_can_stop_list_scroll(cx: &mut TestAppContext) {
1898 let cx = cx.add_empty_window();
1899
1900 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1901 let child_saw_event = Rc::new(Cell::new(false));
1902
1903 struct TestView {
1904 state: ListState,
1905 child_saw_event: Rc<Cell<bool>>,
1906 }
1907 impl Render for TestView {
1908 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1909 let child_saw_event = self.child_saw_event.clone();
1910 list(self.state.clone(), move |_, _, _| {
1911 let child_saw_event = child_saw_event.clone();
1912 div()
1913 .h(px(20.))
1914 .w_full()
1915 .on_scroll(move |_, _, cx| {
1916 child_saw_event.set(true);
1917 cx.stop_propagation();
1918 })
1919 .into_any()
1920 })
1921 .w_full()
1922 .h_full()
1923 }
1924 }
1925
1926 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
1927 cx.new(|_| TestView {
1928 state: state.clone(),
1929 child_saw_event: child_saw_event.clone(),
1930 })
1931 .into_any_element()
1932 });
1933
1934 cx.simulate_event(ScrollEvent {
1935 position: point(px(50.), px(10.)),
1936 delta: ScrollDelta::Pixels(point(px(0.), px(-30.))),
1937 ..Default::default()
1938 });
1939
1940 assert!(
1941 child_saw_event.get(),
1942 "the child's scroll-wheel handler should run"
1943 );
1944 let offset = state.logical_scroll_top();
1946 assert_eq!(offset.item_ix, 0);
1947 assert_eq!(offset.offset_in_item, px(0.));
1948 }
1949
1950 #[gpui::test]
1951 fn test_item_viewport_queries_return_none_before_layout(_cx: &mut TestAppContext) {
1952 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1953
1954 assert_eq!(state.item_is_above_viewport(0), None);
1955 assert_eq!(state.item_is_below_viewport(0), None);
1956 }
1957
1958 #[gpui::test]
1959 fn test_item_viewport_queries_before_logical_scroll_top(cx: &mut TestAppContext) {
1960 let cx = cx.add_empty_window();
1961
1962 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1963
1964 state.scroll_to(gpui::ListOffset {
1965 item_ix: 2,
1966 offset_in_item: px(0.),
1967 });
1968 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1969 cx.new(|_| TestListView(state.clone())).into_any_element()
1970 });
1971
1972 assert_eq!(state.item_is_above_viewport(1), Some(true));
1973 assert_eq!(state.item_is_below_viewport(1), Some(false));
1974 }
1975
1976 #[gpui::test]
1977 fn test_item_viewport_queries_measured_item_inside_viewport(cx: &mut TestAppContext) {
1978 let cx = cx.add_empty_window();
1979
1980 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1981
1982 state.scroll_to(gpui::ListOffset {
1983 item_ix: 2,
1984 offset_in_item: px(0.),
1985 });
1986 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1987 cx.new(|_| TestListView(state.clone())).into_any_element()
1988 });
1989
1990 assert_eq!(state.item_is_above_viewport(2), Some(false));
1991 assert_eq!(state.item_is_below_viewport(2), Some(false));
1992 }
1993
1994 #[gpui::test]
1995 fn test_item_viewport_queries_measured_item_above_viewport(cx: &mut TestAppContext) {
1996 let cx = cx.add_empty_window();
1997
1998 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1999
2000 state.scroll_to(gpui::ListOffset {
2001 item_ix: 2,
2002 offset_in_item: px(20.),
2003 });
2004 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2005 cx.new(|_| TestListView(state.clone())).into_any_element()
2006 });
2007
2008 assert_eq!(state.item_is_above_viewport(2), Some(true));
2009 assert_eq!(state.item_is_below_viewport(2), Some(false));
2010 }
2011
2012 #[gpui::test]
2013 fn test_item_viewport_queries_measured_item_below_viewport(cx: &mut TestAppContext) {
2014 let cx = cx.add_empty_window();
2015
2016 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2017
2018 state.scroll_to(gpui::ListOffset {
2019 item_ix: 2,
2020 offset_in_item: px(0.),
2021 });
2022 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2023 cx.new(|_| TestListView(state.clone())).into_any_element()
2024 });
2025
2026 assert_eq!(state.item_is_above_viewport(3), Some(false));
2027 assert_eq!(state.item_is_below_viewport(3), Some(true));
2028 }
2029
2030 #[gpui::test]
2031 fn test_item_viewport_queries_remain_stable_with_zero_height_viewport(cx: &mut TestAppContext) {
2032 let cx = cx.add_empty_window();
2033
2034 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2035
2036 state.scroll_to(gpui::ListOffset {
2037 item_ix: 2,
2038 offset_in_item: px(0.),
2039 });
2040 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2041 cx.new(|_| TestListView(state.clone())).into_any_element()
2042 });
2043
2044 assert_eq!(state.item_is_above_viewport(3), Some(false));
2045 assert_eq!(state.item_is_below_viewport(3), Some(true));
2046
2047 cx.draw(point(px(0.), px(0.)), size(px(100.), px(0.)), |_, cx| {
2052 cx.new(|_| TestListView(state.clone())).into_any_element()
2053 });
2054
2055 assert_eq!(state.item_is_above_viewport(1), Some(true));
2056 assert_eq!(state.item_is_below_viewport(1), Some(false));
2057 assert_eq!(state.item_is_above_viewport(3), Some(false));
2058 assert_eq!(state.item_is_below_viewport(3), Some(true));
2059 }
2060
2061 #[gpui::test]
2062 fn test_item_viewport_queries_after_scroll_to_end_before_layout(cx: &mut TestAppContext) {
2063 let cx = cx.add_empty_window();
2064
2065 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2066
2067 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2068 cx.new(|_| TestListView(state.clone())).into_any_element()
2069 });
2070
2071 state.scroll_to_end();
2072
2073 assert_eq!(state.logical_scroll_top().item_ix, state.item_count());
2074 assert_eq!(state.item_is_above_viewport(0), Some(true));
2075 assert_eq!(state.item_is_below_viewport(0), Some(false));
2076 }
2077
2078 #[gpui::test]
2079 fn test_measure_all_after_width_change(cx: &mut TestAppContext) {
2080 let cx = cx.add_empty_window();
2081
2082 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
2083
2084 struct TestView(ListState);
2085 impl Render for TestView {
2086 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2087 list(self.0.clone(), |_, _, _| {
2088 div().h(px(50.)).w_full().into_any()
2089 })
2090 .w_full()
2091 .h_full()
2092 }
2093 }
2094
2095 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2096
2097 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2100 view.clone().into_any_element()
2101 });
2102 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
2103
2104 cx.draw(point(px(0.), px(0.)), size(px(200.), px(200.)), |_, _| {
2108 view.into_any_element()
2109 });
2110 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
2111 }
2112
2113 #[gpui::test]
2114 fn test_remeasure(cx: &mut TestAppContext) {
2115 let cx = cx.add_empty_window();
2116
2117 let item_height = Rc::new(Cell::new(100usize));
2121 let state = ListState::new(10, crate::ListAlignment::Top, px(10.));
2122
2123 struct TestView {
2124 state: ListState,
2125 item_height: Rc<Cell<usize>>,
2126 }
2127
2128 impl Render for TestView {
2129 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2130 let height = self.item_height.get();
2131 list(self.state.clone(), move |_, _, _| {
2132 div().h(px(height as f32)).w_full().into_any()
2133 })
2134 .w_full()
2135 .h_full()
2136 }
2137 }
2138
2139 let state_clone = state.clone();
2140 let item_height_clone = item_height.clone();
2141 let view = cx.update(|_, cx| {
2142 cx.new(|_| TestView {
2143 state: state_clone,
2144 item_height: item_height_clone,
2145 })
2146 });
2147
2148 state.scroll_to(gpui::ListOffset {
2151 item_ix: 2,
2152 offset_in_item: px(40.),
2153 });
2154
2155 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2156 view.clone().into_any_element()
2157 });
2158
2159 let offset = state.logical_scroll_top();
2160 assert_eq!(offset.item_ix, 2);
2161 assert_eq!(offset.offset_in_item, px(40.));
2162
2163 item_height.set(50);
2168 state.remeasure();
2169
2170 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2171 view.into_any_element()
2172 });
2173
2174 let offset = state.logical_scroll_top();
2175 assert_eq!(offset.item_ix, 2);
2176 assert_eq!(offset.offset_in_item, px(20.));
2177 }
2178
2179 #[gpui::test]
2180 fn test_remeasure_item_preserves_scroll_offset(cx: &mut TestAppContext) {
2181 let cx = cx.add_empty_window();
2182
2183 let item_height = Rc::new(Cell::new(100usize));
2184 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2185
2186 struct TestView {
2187 state: ListState,
2188 item_height: Rc<Cell<usize>>,
2189 }
2190
2191 impl Render for TestView {
2192 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2193 let height = self.item_height.get();
2194 list(self.state.clone(), move |index, _, _| {
2195 let height = if index == 5 { height } else { 100 };
2196 div().h(px(height as f32)).w_full().into_any()
2197 })
2198 .w_full()
2199 .h_full()
2200 }
2201 }
2202
2203 let state_clone = state.clone();
2204 let item_height_clone = item_height.clone();
2205 let view = cx.update(|_, cx| {
2206 cx.new(|_| TestView {
2207 state: state_clone,
2208 item_height: item_height_clone,
2209 })
2210 });
2211
2212 state.scroll_to(gpui::ListOffset {
2213 item_ix: 5,
2214 offset_in_item: px(40.),
2215 });
2216
2217 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2218 view.clone().into_any_element()
2219 });
2220
2221 item_height.set(200);
2222 state.remeasure_items(5..6);
2223
2224 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2225 view.into_any_element()
2226 });
2227
2228 let offset = state.logical_scroll_top();
2229 assert_eq!(offset.item_ix, 5);
2230 assert_eq!(offset.offset_in_item, px(40.));
2231 }
2232
2233 #[gpui::test]
2234 fn test_provisional_item_heights_keep_scroll_anchor(cx: &mut TestAppContext) {
2235 let cx = cx.add_empty_window();
2236 let state = ListState::new(20, crate::ListAlignment::Top, px(100.));
2237 let painted = Rc::new(RefCell::new(Vec::new()));
2238 let first_rendered = Rc::new(Cell::new(None::<usize>));
2239 let ready = item_readiness(&state, false);
2240 state.set_follow_mode(FollowMode::Tail);
2241 state.scroll_to(list_offset(10, 40.));
2242
2243 for (height, measurement_ready) in [(0., false), (100., false), (100., true)] {
2244 ready.set(measurement_ready);
2245 painted.borrow_mut().clear();
2246 first_rendered.set(None);
2247 draw_list(
2248 &state,
2249 200.,
2250 {
2251 let painted = painted.clone();
2252 let first_rendered = first_rendered.clone();
2253 move |index| {
2254 first_rendered.set(Some(first_rendered.get().unwrap_or(index).min(index)));
2255 painted_item(index, height, &painted)
2256 }
2257 },
2258 cx,
2259 );
2260 assert_eq!(state.logical_scroll_top(), list_offset(10, 40.));
2261 assert!(!state.is_following_tail());
2262 assert_eq!(
2263 first_rendered.get(),
2264 Some(if measurement_ready { 9 } else { 10 })
2265 );
2266 if height > 0. {
2267 assert_eq!(
2268 painted.borrow().as_slice(),
2269 &[
2270 (10, px(-40.), px(60.)),
2271 (11, px(60.), px(160.)),
2272 (12, px(160.), px(260.)),
2273 ]
2274 );
2275 }
2276 }
2277 }
2278
2279 #[gpui::test]
2280 fn test_unready_measurements_preserve_pending_fraction(cx: &mut TestAppContext) {
2281 let cx = cx.add_empty_window();
2282 for (provisional_height, destination, final_height, expected_offset) in [
2283 (Some(200.), None, 200., 100.),
2284 (None, Some(40.), 200., 80.),
2285 (Some(200.), Some(50.), 200., 50.),
2286 (Some(0.), Some(40.), 20., 20.),
2287 (Some(10.), Some(40.), 100., 40.),
2288 (Some(200.), Some(50.), 300., 50.),
2289 ] {
2290 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2291 let painted = Rc::new(RefCell::new(Vec::new()));
2292 let draw = |height, cx: &mut VisualTestContext| {
2293 painted.borrow_mut().clear();
2294 let painted = painted.clone();
2295 draw_list(
2296 &state,
2297 200.,
2298 move |index| {
2299 painted_item(index, if index == 5 { height } else { 100. }, &painted)
2300 },
2301 cx,
2302 );
2303 };
2304 let ready = item_readiness(&state, true);
2305 state.scroll_to(list_offset(5, 50.));
2306 draw(100., cx);
2307 ready.set(false);
2308 state.remeasure();
2309 if let Some(height) = provisional_height {
2310 for height in [0., height, height] {
2311 draw(height, cx);
2312 assert_eq!(state.logical_scroll_top(), list_offset(5, 50.));
2313 }
2314 }
2315 if let Some(offset) = destination {
2316 state.scroll_to(list_offset(5, offset));
2317 }
2318 ready.set(true);
2319 for _ in 0..2 {
2320 draw(final_height, cx);
2321 assert_eq!(state.logical_scroll_top(), list_offset(5, expected_offset));
2322 assert_eq!(
2323 painted.borrow().first().copied(),
2324 Some((5, px(-expected_offset), px(final_height - expected_offset)))
2325 );
2326 }
2327 }
2328 }
2329
2330 #[gpui::test]
2331 fn test_navigation_cancels_provisional_and_paints_destination(cx: &mut TestAppContext) {
2332 let cx = cx.add_empty_window();
2333 for measured in [false, true] {
2334 let navigations: [(&str, fn(&ListState), usize, f32); 7] = [
2335 (
2336 "scroll_to",
2337 |state| state.scroll_to(list_offset(12, 0.)),
2338 12,
2339 0.,
2340 ),
2341 ("end", ListState::scroll_to_end, 19, 150.),
2342 ("down", |state| state.scroll_by(px(5000.)), 19, 150.),
2343 (
2344 "up",
2345 |state| state.scroll_by(px(-5000.)),
2346 if measured { 0 } else { 6 },
2347 0.,
2348 ),
2349 (
2350 "reveal_below",
2351 |state| state.scroll_to_reveal_item(10),
2352 10,
2353 150.,
2354 ),
2355 (
2356 "reveal_above",
2357 |state| state.scroll_to_reveal_item(0),
2358 0,
2359 0.,
2360 ),
2361 (
2362 "follow_tail",
2363 |state| state.set_follow_mode(FollowMode::Tail),
2364 19,
2365 150.,
2366 ),
2367 ];
2368 for (name, navigate, destination, top) in navigations {
2369 let state = ListState::new(20, crate::ListAlignment::Top, px(0.));
2370 let state = if measured { state.measure_all() } else { state };
2371 let painted = Rc::new(RefCell::new(Vec::new()));
2372 let draw = |cx: &mut VisualTestContext| {
2373 painted.borrow_mut().clear();
2374 let painted = painted.clone();
2375 draw_list(
2376 &state,
2377 200.,
2378 move |index| {
2379 painted_item(index, if index == 5 { 0. } else { 50. }, &painted)
2380 },
2381 cx,
2382 );
2383 };
2384 state.set_item_measurement_readiness(|index, _| index != 5);
2385 state.scroll_to(list_offset(5, 20.));
2386 state.remeasure_items(5..6);
2387 draw(cx);
2388 assert!(state.0.borrow().is_provisional(), "{name}");
2389 navigate(&state);
2390 assert!(!state.0.borrow().is_provisional(), "{name}");
2391 draw(cx);
2392 assert!(!state.0.borrow().is_provisional(), "{name}");
2393 assert_eq!(
2394 painted
2395 .borrow()
2396 .iter()
2397 .find(|(index, _, _)| *index == destination)
2398 .copied(),
2399 Some((destination, px(top), px(top + 50.))),
2400 "{name}, measured={measured}",
2401 );
2402 }
2403 }
2404
2405 let navigations: [(&str, fn(&ListState)); 6] = [
2406 ("end_sentinel", |state| state.scroll_to(list_offset(3, 0.))),
2407 ("scroll_to_end", ListState::scroll_to_end),
2408 ("follow_tail", |state| {
2409 state.set_follow_mode(FollowMode::Tail)
2410 }),
2411 ("horizontal", ListState::scroll_to_end),
2412 ("pause_tail", |state| {
2413 state.set_follow_mode(FollowMode::Tail)
2414 }),
2415 ("disable_tail", |state| {
2416 state.set_follow_mode(FollowMode::Tail)
2417 }),
2418 ];
2419 for (name, navigate) in navigations {
2420 let state = ListState::new(3, crate::ListAlignment::Top, px(0.)).measure_all();
2421 let ready = item_readiness(&state, false);
2422 let painted = Rc::new(RefCell::new(Vec::new()));
2423 let draw = |height, cx: &mut VisualTestContext| {
2424 painted.borrow_mut().clear();
2425 let painted = painted.clone();
2426 draw_list(
2427 &state,
2428 200.,
2429 move |index| painted_item(index, height, &painted),
2430 cx,
2431 );
2432 };
2433 navigate(&state);
2434 for (height, measurement_ready, expected) in [
2435 (
2436 100.,
2437 false,
2438 [(1, px(0.), px(100.)), (2, px(100.), px(200.))],
2439 ),
2440 (
2441 150.,
2442 true,
2443 [(1, px(-100.), px(50.)), (2, px(50.), px(200.))],
2444 ),
2445 ] {
2446 if measurement_ready {
2447 match name {
2448 "horizontal" => cx.simulate_event(ScrollEvent {
2449 position: point(px(50.), px(100.)),
2450 delta: ScrollDelta::Pixels(point(px(20.), px(0.))),
2451 ..ScrollEvent::default()
2452 }),
2453 "pause_tail" => state.pause_following_tail(),
2454 "disable_tail" => state.set_follow_mode(FollowMode::Normal),
2455 _ => {}
2456 }
2457 }
2458 ready.set(measurement_ready);
2459 draw(height, cx);
2460 if !measurement_ready {
2461 assert_eq!(
2462 state.scroll_px_offset_for_scrollbar().y,
2463 px(-100.),
2464 "{name}"
2465 );
2466 assert_eq!(state.logical_scroll_top(), list_offset(1, 0.), "{name}");
2467 assert_eq!(state.item_is_above_viewport(2), None, "{name}");
2468 }
2469 let paused = name == "pause_tail" || name == "disable_tail";
2470 let expected = if measurement_ready && paused {
2471 [(1, px(0.), px(150.)), (2, px(150.), px(300.))]
2472 } else {
2473 expected
2474 };
2475 assert_eq!(
2476 state.is_following_tail(),
2477 name == "follow_tail" || (paused && !measurement_ready),
2478 "{name}"
2479 );
2480 assert_eq!(painted.borrow().as_slice(), &expected, "{name}");
2481 }
2482 ready.set(false);
2483 navigate(&state);
2484 draw(150., cx);
2485 state.scroll_by(px(-20.));
2486 draw(150., cx);
2487 assert_eq!(state.logical_scroll_top(), list_offset(1, 80.), "{name}");
2488 assert!(!state.is_following_tail(), "{name}");
2489 assert_eq!(
2490 painted.borrow().as_slice(),
2491 &[(1, px(-80.), px(70.)), (2, px(70.), px(220.)),],
2492 "{name}"
2493 );
2494 }
2495 }
2496
2497 #[gpui::test]
2498 fn test_unready_overdraw_and_backfill_preserve_scroll_anchor(cx: &mut TestAppContext) {
2499 let cx = cx.add_empty_window();
2500 for (overdraw, anchor, unready_item) in
2501 [(100., list_offset(0, 0.), 2), (0., list_offset(2, 40.), 1)]
2502 {
2503 let state = ListState::new(3, crate::ListAlignment::Top, px(overdraw));
2504 let ready = Rc::new(Cell::new(false));
2505 state.set_item_measurement_readiness({
2506 let ready = ready.clone();
2507 move |index, _| index != unready_item || ready.get()
2508 });
2509 let painted = Rc::new(RefCell::new(Vec::new()));
2510 let draw = |height, cx: &mut VisualTestContext| {
2511 painted.borrow_mut().clear();
2512 let painted = painted.clone();
2513 draw_list(
2514 &state,
2515 200.,
2516 move |index| {
2517 let height = if index == unready_item { height } else { 100. };
2518 painted_item(index, height, &painted)
2519 },
2520 cx,
2521 );
2522 };
2523 state.set_follow_mode(FollowMode::Tail);
2524 state.scroll_to(anchor);
2525 draw(0., cx);
2526 assert_eq!(state.logical_scroll_top(), anchor);
2527 assert!(!state.is_following_tail());
2528 assert_eq!(
2529 *painted.borrow(),
2530 if unready_item == 2 {
2531 vec![(0, px(0.), px(100.)), (1, px(100.), px(200.))]
2532 } else {
2533 vec![(2, px(-40.), px(60.))]
2534 }
2535 );
2536 ready.set(true);
2537 draw(500., cx);
2538 if unready_item == 2 {
2539 assert_eq!(state.logical_scroll_top(), anchor);
2540 assert!(!state.is_following_tail());
2541 assert_eq!(state.max_offset_for_scrollbar().y, px(500.));
2542 assert_eq!(
2543 painted.borrow().as_slice(),
2544 &[(0, px(0.), px(100.)), (1, px(100.), px(200.))]
2545 );
2546 } else {
2547 assert_eq!(state.logical_scroll_top(), list_offset(1, 400.));
2548 assert!(state.is_following_tail());
2549 assert_eq!(
2550 painted.borrow().as_slice(),
2551 &[(1, px(-400.), px(100.)), (2, px(100.), px(200.))]
2552 );
2553 }
2554 }
2555 }
2556
2557 #[gpui::test]
2558 fn test_unready_overdraw_stops_at_first_pending_item(cx: &mut TestAppContext) {
2559 let cx = cx.add_empty_window();
2560 for anchor in [0, 10] {
2561 let state = ListState::new(13, crate::ListAlignment::Top, px(100.));
2562 let ready = Rc::new(Cell::new(false));
2563 let initially_ready = move |index| {
2564 if anchor == 0 { index < 2 } else { index >= 10 }
2565 };
2566 state.set_item_measurement_readiness({
2567 let ready = ready.clone();
2568 move |index, _| initially_ready(index) || ready.get()
2569 });
2570 state.scroll_to(list_offset(anchor, 0.));
2571 let rendered = Rc::new(RefCell::new(Vec::new()));
2572 let painted = Rc::new(RefCell::new(Vec::new()));
2573 let expected_frames = if anchor == 0 {
2574 [vec![0, 1, 2], vec![0, 1, 2], vec![0, 1, 2], vec![0, 1]]
2575 } else {
2576 [
2577 vec![10, 11, 12, 9],
2578 vec![10, 11, 9],
2579 vec![10, 11, 9],
2580 vec![10, 11],
2581 ]
2582 };
2583 for (frame, expected_rendered) in expected_frames.into_iter().enumerate() {
2584 let measurement_ready = frame >= 2;
2585 ready.set(measurement_ready);
2586 rendered.borrow_mut().clear();
2587 painted.borrow_mut().clear();
2588 draw_list(
2589 &state,
2590 200.,
2591 {
2592 let rendered = rendered.clone();
2593 let painted = painted.clone();
2594 move |index| {
2595 rendered.borrow_mut().push(index);
2596 let height = if initially_ready(index) || measurement_ready {
2597 100.
2598 } else {
2599 0.
2600 };
2601 painted_item(index, height, &painted)
2602 }
2603 },
2604 cx,
2605 );
2606 assert_eq!(*rendered.borrow(), expected_rendered);
2607 assert_eq!(state.item_count(), 13);
2608 assert_eq!(state.logical_scroll_top(), list_offset(anchor, 0.));
2609 assert_eq!(
2610 painted.borrow().as_slice(),
2611 &[(anchor, px(0.), px(100.)), (anchor + 1, px(100.), px(200.))]
2612 );
2613 }
2614 }
2615 }
2616
2617 #[gpui::test]
2618 fn test_scrollbar_and_reset_do_not_resurrect_provisional(cx: &mut TestAppContext) {
2619 let cx = cx.add_empty_window();
2620 let state = ListState::new(20, crate::ListAlignment::Top, px(0.));
2621 let painted = Rc::new(RefCell::new(Vec::new()));
2622 let draw = |cx: &mut VisualTestContext| {
2623 painted.borrow_mut().clear();
2624 let painted = painted.clone();
2625 draw_list(
2626 &state,
2627 200.,
2628 move |index| painted_item(index, if index == 5 { 0. } else { 50. }, &painted),
2629 cx,
2630 );
2631 };
2632 state.set_item_measurement_readiness(|index, _| index != 5);
2633 assert!(!state.0.borrow().is_provisional());
2634 state.set_follow_mode(FollowMode::Tail);
2635 for intervening_frame in [false, true] {
2636 state.scroll_to(list_offset(5, 20.));
2637 state.remeasure_items(5..6);
2638 draw(cx);
2639 state.scrollbar_drag_started();
2640 if intervening_frame {
2641 draw(cx);
2642 assert!(state.0.borrow().is_provisional());
2643 }
2644 state.set_offset_from_scrollbar(point(px(0.), px(-10_000.)));
2645 assert!(!state.0.borrow().is_provisional());
2646 state.scrollbar_drag_ended();
2647 assert!(!state.is_scrollbar_dragging());
2648 draw(cx);
2649 assert_eq!(
2650 painted.borrow().last().copied(),
2651 Some((19, px(150.), px(200.)))
2652 );
2653 assert!(!state.0.borrow().is_provisional());
2654 state.scroll_to(list_offset(5, 20.));
2655 assert!(!state.0.borrow().is_provisional());
2656 draw(cx);
2657 assert!(state.0.borrow().is_provisional());
2658 assert_eq!(state.logical_scroll_top(), list_offset(5, 20.));
2659 }
2660 state.remeasure_items(5..6);
2661 state.reset(20);
2662 assert!(!state.0.borrow().is_provisional());
2663 draw(cx);
2664 assert!(!state.0.borrow().is_provisional());
2665 assert_eq!(state.logical_scroll_top(), ListOffset::default());
2666 assert_eq!(
2667 painted.borrow().first().copied(),
2668 Some((0, px(0.), px(50.)))
2669 );
2670 }
2671
2672 #[gpui::test]
2673 fn test_splice_preserves_only_surviving_scroll_offsets(cx: &mut TestAppContext) {
2674 let cx = cx.add_empty_window();
2675 let painted = Rc::new(RefCell::new(Vec::new()));
2676 let draw = |state: &ListState, height, cx: &mut VisualTestContext| {
2677 painted.borrow_mut().clear();
2678 let painted = painted.clone();
2679 draw_list(
2680 state,
2681 200.,
2682 move |index| painted_item(index, height, &painted),
2683 cx,
2684 );
2685 };
2686 for (provisional, replacement_count) in [(true, 0), (false, 1)] {
2687 let state = ListState::new(20, crate::ListAlignment::Top, px(0.));
2688 let ready = item_readiness(&state, !provisional);
2689 state.scroll_to(list_offset(8, 50.));
2690 state.remeasure_items(8..9);
2691 if provisional {
2692 draw(&state, 100., cx);
2693 }
2694 state.splice(8..9, replacement_count);
2695 assert!(!state.0.borrow().is_provisional());
2696 ready.set(true);
2697 draw(&state, 100., cx);
2698 assert_eq!(state.logical_scroll_top(), list_offset(8, 0.));
2699 assert_eq!(
2700 painted.borrow().first().copied(),
2701 Some((8, px(0.), px(100.)))
2702 );
2703 }
2704 let state = ListState::new(20, crate::ListAlignment::Top, px(0.)).measure_all();
2705 state.scroll_to(list_offset(8, 50.));
2706 draw(&state, 100., cx);
2707 state.remeasure();
2708 state.splice(0..2, 0);
2709 draw(&state, 80., cx);
2710 assert_eq!(state.logical_scroll_top(), list_offset(6, 40.));
2711 assert_eq!(
2712 painted.borrow().first().copied(),
2713 Some((6, px(-40.), px(40.)))
2714 );
2715 let ready = item_readiness(&state, false);
2716 state.remeasure_items(6..7);
2717 state.splice(8..10, 3);
2718 draw(&state, 100., cx);
2719 assert!(state.0.borrow().is_provisional());
2720 assert_eq!(state.logical_scroll_top(), list_offset(6, 40.));
2721 state.splice(0..0, 2);
2722 assert!(!state.0.borrow().is_provisional());
2723 draw(&state, 100., cx);
2724 assert!(state.0.borrow().is_provisional());
2725 assert_eq!(state.logical_scroll_top(), list_offset(8, 40.));
2726 ready.set(true);
2727 draw(&state, 100., cx);
2728 assert_eq!(state.logical_scroll_top(), list_offset(8, 40.));
2729 assert_eq!(
2730 painted.borrow().first().copied(),
2731 Some((8, px(-40.), px(60.)))
2732 );
2733 }
2734
2735 #[gpui::test]
2736 fn test_child_autoscroll_overrides_pending_and_release_fills_viewport(cx: &mut TestAppContext) {
2737 let cx = cx.add_empty_window();
2738 for (count, child, request, expected, released) in [
2739 (10, 2, 40.0..45.0, list_offset(2, 40.), list_offset(2, 40.)),
2740 (3, 2, 40.0..45.0, list_offset(2, 40.), list_offset(1, 0.)),
2741 (10, 4, 80.0..90.0, list_offset(2, 90.), list_offset(2, 90.)),
2742 (10, 2, -130.0..5.0, list_offset(0, 70.), list_offset(0, 70.)),
2743 (10, 2, -250.0..5.0, list_offset(0, 0.), list_offset(0, 0.)),
2744 ] {
2745 let state = ListState::new(count, crate::ListAlignment::Top, px(0.));
2746 let requested = Rc::new(Cell::new(false));
2747 let painted = Rc::new(RefCell::new(Vec::new()));
2748 let draw = |cx: &mut VisualTestContext| {
2749 painted.borrow_mut().clear();
2750 let painted = painted.clone();
2751 let requested = requested.clone();
2752 let request = request.clone();
2753 draw_list(
2754 &state,
2755 200.,
2756 move |index| {
2757 let painted = painted.clone();
2758 let requested = requested.clone();
2759 let request = request.clone();
2760 canvas(
2761 move |bounds, window, _| {
2762 if index == child && !requested.replace(true) {
2763 window.request_autoscroll(Bounds::from_corners(
2764 point(bounds.left(), bounds.top() + px(request.start)),
2765 point(bounds.right(), bounds.top() + px(request.end)),
2766 ));
2767 }
2768 },
2769 move |bounds, _, _, _| {
2770 painted
2771 .borrow_mut()
2772 .push((index, bounds.top(), bounds.bottom()));
2773 },
2774 )
2775 .h(px(100.))
2776 .w_full()
2777 .into_any()
2778 },
2779 cx,
2780 );
2781 };
2782 let ready = item_readiness(&state, false);
2783 state.scroll_to(list_offset(2, 50.));
2784 state.remeasure_items(2..3);
2785 for _ in 0..2 {
2786 draw(cx);
2787 assert!(requested.get());
2788 assert!(state.0.borrow().is_provisional());
2789 assert_eq!(state.logical_scroll_top(), expected);
2790 assert_eq!(
2791 painted.borrow().first().copied(),
2792 Some((
2793 expected.item_ix,
2794 -expected.offset_in_item,
2795 px(100.) - expected.offset_in_item,
2796 ))
2797 );
2798 }
2799 ready.set(true);
2800 draw(cx);
2801 assert!(!state.0.borrow().is_provisional());
2802 assert_eq!(state.logical_scroll_top(), released);
2803 let visible_count = if released.offset_in_item == px(0.) {
2804 2
2805 } else {
2806 3
2807 };
2808 assert_eq!(
2809 painted.borrow().as_slice(),
2810 (0..visible_count)
2811 .map(|index| {
2812 let top = px(index as f32 * 100.) - released.offset_in_item;
2813 (released.item_ix + index, top, top + px(100.))
2814 })
2815 .collect::<Vec<_>>()
2816 .as_slice()
2817 );
2818 }
2819 }
2820
2821 #[gpui::test]
2822 fn test_remeasure_then_scroll_does_not_revert_scroll_position(cx: &mut TestAppContext) {
2823 let cx = cx.add_empty_window();
2824
2825 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2826
2827 struct TestView(ListState);
2828 impl Render for TestView {
2829 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2830 list(self.0.clone(), |_, _, _| {
2831 div().h(px(100.)).w_full().into_any()
2832 })
2833 .w_full()
2834 .h_full()
2835 }
2836 }
2837
2838 let view = {
2839 let state = state.clone();
2840 cx.update(|_, cx| cx.new(|_| TestView(state)))
2841 };
2842
2843 state.scroll_to(gpui::ListOffset {
2844 item_ix: 5,
2845 offset_in_item: px(40.),
2846 });
2847
2848 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2849 view.clone().into_any_element()
2850 });
2851
2852 state.remeasure_items(5..6);
2853
2854 cx.simulate_event(ScrollEvent {
2855 position: point(px(50.), px(100.)),
2856 delta: ScrollDelta::Pixels(point(px(0.), px(-30.))),
2857 ..Default::default()
2858 });
2859
2860 let offset = state.logical_scroll_top();
2861 assert_eq!(offset.item_ix, 5);
2862 assert_eq!(offset.offset_in_item, px(70.));
2863
2864 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2865 view.into_any_element()
2866 });
2867
2868 let offset = state.logical_scroll_top();
2869 assert_eq!(offset.item_ix, 5);
2870 assert_eq!(
2871 offset.offset_in_item,
2872 px(70.),
2873 "scrolling after a remeasure should not be reverted by the stale pending scroll"
2874 );
2875 }
2876
2877 #[gpui::test]
2878 fn test_scroll_after_remeasure_clamps_to_shrunk_item_height(cx: &mut TestAppContext) {
2879 let cx = cx.add_empty_window();
2880 for provisional in [false, true] {
2881 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2882 let painted = Rc::new(RefCell::new(Vec::new()));
2883 let draw = |height, cx: &mut VisualTestContext| {
2884 painted.borrow_mut().clear();
2885 let painted = painted.clone();
2886 draw_list(
2887 &state,
2888 200.,
2889 move |index| {
2890 painted_item(index, if index == 5 { height } else { 100. }, &painted)
2891 },
2892 cx,
2893 );
2894 };
2895 let ready = item_readiness(&state, true);
2896 state.scroll_to(list_offset(5, 40.));
2897 draw(100., cx);
2898 ready.set(!provisional);
2899 state.remeasure_items(5..6);
2900 if provisional {
2901 draw(100., cx);
2902 }
2903 ready.set(true);
2904 cx.simulate_event(ScrollEvent {
2905 position: point(px(50.), px(100.)),
2906 delta: ScrollDelta::Pixels(point(px(0.), px(-30.))),
2907 ..ScrollEvent::default()
2908 });
2909 assert_eq!(state.logical_scroll_top(), list_offset(5, 70.));
2910 for _ in 0..2 {
2911 draw(50., cx);
2912 assert_eq!(state.logical_scroll_top(), list_offset(5, 50.));
2913 assert_eq!(
2914 painted
2915 .borrow()
2916 .iter()
2917 .find(|(index, _, _)| *index == 6)
2918 .copied(),
2919 Some((6, px(0.), px(100.)))
2920 );
2921 }
2922 }
2923 }
2924
2925 #[gpui::test]
2926 fn test_follow_tail_stays_at_bottom_as_items_grow(cx: &mut TestAppContext) {
2927 let cx = cx.add_empty_window();
2928
2929 let item_height = Rc::new(Cell::new(50usize));
2932 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2933
2934 struct TestView {
2935 state: ListState,
2936 item_height: Rc<Cell<usize>>,
2937 }
2938 impl Render for TestView {
2939 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2940 let height = self.item_height.get();
2941 list(self.state.clone(), move |_, _, _| {
2942 div().h(px(height as f32)).w_full().into_any()
2943 })
2944 .w_full()
2945 .h_full()
2946 }
2947 }
2948
2949 let state_clone = state.clone();
2950 let item_height_clone = item_height.clone();
2951 let view = cx.update(|_, cx| {
2952 cx.new(|_| TestView {
2953 state: state_clone,
2954 item_height: item_height_clone,
2955 })
2956 });
2957
2958 state.set_follow_mode(FollowMode::Tail);
2959
2960 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2963 view.clone().into_any_element()
2964 });
2965
2966 let offset = state.logical_scroll_top();
2969 assert_eq!(offset.item_ix, 6);
2970 assert_eq!(offset.offset_in_item, px(0.));
2971 assert!(state.is_following_tail());
2972
2973 item_height.set(80);
2976 state.remeasure();
2977
2978 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2979 view.into_any_element()
2980 });
2981
2982 let offset = state.logical_scroll_top();
2989 assert_eq!(offset.item_ix, 7);
2990 assert_eq!(offset.offset_in_item, px(40.));
2991 assert!(state.is_following_tail());
2992 }
2993
2994 #[gpui::test]
2995 fn test_pause_following_tail_reengages_when_still_at_bottom(cx: &mut TestAppContext) {
2996 let cx = cx.add_empty_window();
2997
2998 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
3000
3001 struct TestView(ListState);
3002 impl Render for TestView {
3003 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3004 list(self.0.clone(), |_, _, _| {
3005 div().h(px(50.)).w_full().into_any()
3006 })
3007 .w_full()
3008 .h_full()
3009 }
3010 }
3011
3012 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3013 state.set_follow_mode(FollowMode::Tail);
3014
3015 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3016 view.clone().into_any_element()
3017 });
3018 assert!(state.is_following_tail());
3019
3020 state.pause_following_tail();
3024 assert!(!state.is_following_tail());
3025
3026 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3027 view.into_any_element()
3028 });
3029 assert!(
3030 state.is_following_tail(),
3031 "pausing while at the bottom must re-engage follow-tail on the next layout"
3032 );
3033 }
3034
3035 #[gpui::test]
3036 fn test_pause_following_tail_freezes_off_bottom(cx: &mut TestAppContext) {
3037 let cx = cx.add_empty_window();
3038
3039 let item_height = Rc::new(Cell::new(50usize));
3042 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
3043
3044 struct TestView {
3045 state: ListState,
3046 item_height: Rc<Cell<usize>>,
3047 }
3048 impl Render for TestView {
3049 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3050 let height = self.item_height.get();
3051 list(self.state.clone(), move |_, _, _| {
3052 div().h(px(height as f32)).w_full().into_any()
3053 })
3054 .w_full()
3055 .h_full()
3056 }
3057 }
3058
3059 let view = cx.update(|_, cx| {
3060 cx.new(|_| TestView {
3061 state: state.clone(),
3062 item_height: item_height.clone(),
3063 })
3064 });
3065 state.set_follow_mode(FollowMode::Tail);
3066
3067 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3069 view.clone().into_any_element()
3070 });
3071 assert_eq!(state.logical_scroll_top().item_ix, 6);
3072 assert!(state.is_following_tail());
3073
3074 state.pause_following_tail();
3078 item_height.set(80);
3079 state.remeasure();
3080 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3081 view.clone().into_any_element()
3082 });
3083 let offset = state.logical_scroll_top();
3084 assert_eq!(offset.item_ix, 6);
3085 assert_eq!(offset.offset_in_item, px(0.));
3086 assert!(
3087 !state.is_following_tail(),
3088 "a paused list must not re-engage while the frozen top is off the bottom"
3089 );
3090
3091 item_height.set(50);
3094 state.remeasure();
3095 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3096 view.into_any_element()
3097 });
3098 assert!(
3099 state.is_following_tail(),
3100 "returning to the bottom must restore follow-tail"
3101 );
3102 }
3103
3104 #[gpui::test]
3105 fn test_follow_tail_disengages_on_user_scroll(cx: &mut TestAppContext) {
3106 let cx = cx.add_empty_window();
3107
3108 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
3110
3111 struct TestView(ListState);
3112 impl Render for TestView {
3113 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3114 list(self.0.clone(), |_, _, _| {
3115 div().h(px(50.)).w_full().into_any()
3116 })
3117 .w_full()
3118 .h_full()
3119 }
3120 }
3121
3122 state.set_follow_mode(FollowMode::Tail);
3123
3124 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, cx| {
3126 cx.new(|_| TestView(state.clone())).into_any_element()
3127 });
3128 assert!(state.is_following_tail());
3129
3130 cx.simulate_event(ScrollEvent {
3133 position: point(px(50.), px(100.)),
3134 delta: ScrollDelta::Pixels(point(px(0.), px(100.))),
3135 ..Default::default()
3136 });
3137
3138 assert!(
3139 !state.is_following_tail(),
3140 "follow-tail should disengage when the user scrolls toward the start"
3141 );
3142 }
3143
3144 #[gpui::test]
3145 fn test_follow_tail_disengages_on_scrollbar_reposition(cx: &mut TestAppContext) {
3146 let cx = cx.add_empty_window();
3147
3148 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3150
3151 struct TestView(ListState);
3152 impl Render for TestView {
3153 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3154 list(self.0.clone(), |_, _, _| {
3155 div().h(px(50.)).w_full().into_any()
3156 })
3157 .w_full()
3158 .h_full()
3159 }
3160 }
3161
3162 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3163
3164 state.set_follow_mode(FollowMode::Tail);
3165
3166 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3168 view.clone().into_any_element()
3169 });
3170 assert!(state.is_following_tail());
3171
3172 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
3174
3175 let offset = state.logical_scroll_top();
3176 assert_eq!(offset.item_ix, 3);
3177 assert_eq!(offset.offset_in_item, px(0.));
3178 assert!(
3179 !state.is_following_tail(),
3180 "follow-tail should disengage when the scrollbar manually repositions the list"
3181 );
3182
3183 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3186 view.into_any_element()
3187 });
3188
3189 let offset = state.logical_scroll_top();
3190 assert_eq!(offset.item_ix, 3);
3191 assert_eq!(offset.offset_in_item, px(0.));
3192 }
3193
3194 #[gpui::test]
3195 fn test_scrollbar_drag_with_growing_content(cx: &mut TestAppContext) {
3196 let cx = cx.add_empty_window();
3197
3198 let last_item_height = Rc::new(Cell::new(50usize));
3199 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3200
3201 struct TestView {
3202 state: ListState,
3203 last_item_height: Rc<Cell<usize>>,
3204 }
3205 impl Render for TestView {
3206 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3207 let last_item_height = self.last_item_height.clone();
3208 list(self.state.clone(), move |index, _, _| {
3209 let height = if index == 9 {
3210 last_item_height.get()
3211 } else {
3212 50
3213 };
3214 div().h(px(height as f32)).w_full().into_any()
3215 })
3216 .w_full()
3217 .h_full()
3218 }
3219 }
3220
3221 let view = cx.update(|_, cx| {
3222 cx.new(|_| TestView {
3223 state: state.clone(),
3224 last_item_height: last_item_height.clone(),
3225 })
3226 });
3227
3228 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3229 view.clone().into_any_element()
3230 });
3231
3232 state.scrollbar_drag_started();
3233
3234 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
3235 let scrollbar_offset_before_growth = state.scroll_px_offset_for_scrollbar();
3236
3237 let offset = state.logical_scroll_top();
3238 assert_eq!(offset.item_ix, 3);
3239 assert_eq!(offset.offset_in_item, px(0.));
3240
3241 last_item_height.set(550);
3242 state.remeasure_items(9..10);
3243 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3244 view.clone().into_any_element()
3245 });
3246
3247 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
3248 assert_eq!(
3249 state.scroll_px_offset_for_scrollbar(),
3250 scrollbar_offset_before_growth
3251 );
3252
3253 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
3254 let offset = state.logical_scroll_top();
3255 assert_eq!(offset.item_ix, 3);
3256 assert_eq!(offset.offset_in_item, px(0.));
3257 }
3258
3259 #[gpui::test]
3260 fn test_set_follow_tail_snaps_to_bottom(cx: &mut TestAppContext) {
3261 let cx = cx.add_empty_window();
3262
3263 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
3265
3266 struct TestView(ListState);
3267 impl Render for TestView {
3268 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3269 list(self.0.clone(), |_, _, _| {
3270 div().h(px(50.)).w_full().into_any()
3271 })
3272 .w_full()
3273 .h_full()
3274 }
3275 }
3276
3277 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3278
3279 state.scroll_to(gpui::ListOffset {
3281 item_ix: 3,
3282 offset_in_item: px(0.),
3283 });
3284
3285 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3286 view.clone().into_any_element()
3287 });
3288
3289 let offset = state.logical_scroll_top();
3290 assert_eq!(offset.item_ix, 3);
3291 assert_eq!(offset.offset_in_item, px(0.));
3292 assert!(!state.is_following_tail());
3293
3294 state.set_follow_mode(FollowMode::Tail);
3297
3298 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3299 view.into_any_element()
3300 });
3301
3302 let offset = state.logical_scroll_top();
3305 assert_eq!(offset.item_ix, 6);
3306 assert_eq!(offset.offset_in_item, px(0.));
3307 assert!(state.is_following_tail());
3308 }
3309
3310 #[gpui::test]
3311 fn test_bottom_aligned_scrollbar_offset_at_end(cx: &mut TestAppContext) {
3312 let cx = cx.add_empty_window();
3313
3314 const ITEMS: usize = 10;
3315 const ITEM_SIZE: f32 = 50.0;
3316
3317 let state = ListState::new(
3318 ITEMS,
3319 crate::ListAlignment::Bottom,
3320 px(ITEMS as f32 * ITEM_SIZE),
3321 );
3322
3323 struct TestView(ListState);
3324 impl Render for TestView {
3325 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3326 list(self.0.clone(), |_, _, _| {
3327 div().h(px(ITEM_SIZE)).w_full().into_any()
3328 })
3329 .w_full()
3330 .h_full()
3331 }
3332 }
3333
3334 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
3335 cx.new(|_| TestView(state.clone())).into_any_element()
3336 });
3337
3338 assert_eq!(state.logical_scroll_top().item_ix, ITEMS);
3341
3342 let max_offset = state.max_offset_for_scrollbar();
3343 let scroll_offset = state.scroll_px_offset_for_scrollbar();
3344
3345 assert_eq!(
3346 -scroll_offset.y, max_offset.y,
3347 "scrollbar offset ({}) should equal max offset ({}) when list is pinned to bottom",
3348 -scroll_offset.y, max_offset.y,
3349 );
3350 }
3351
3352 #[gpui::test]
3356 fn test_follow_tail_reengages_when_scrolled_back_to_bottom(cx: &mut TestAppContext) {
3357 let cx = cx.add_empty_window();
3358
3359 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
3361
3362 struct TestView(ListState);
3363 impl Render for TestView {
3364 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3365 list(self.0.clone(), |_, _, _| {
3366 div().h(px(50.)).w_full().into_any()
3367 })
3368 .w_full()
3369 .h_full()
3370 }
3371 }
3372
3373 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3374
3375 state.set_follow_mode(FollowMode::Tail);
3376
3377 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3378 view.clone().into_any_element()
3379 });
3380 assert!(state.is_following_tail());
3381
3382 cx.simulate_event(ScrollEvent {
3384 position: point(px(50.), px(100.)),
3385 delta: ScrollDelta::Pixels(point(px(0.), px(50.))),
3386 ..Default::default()
3387 });
3388 assert!(!state.is_following_tail());
3389
3390 cx.simulate_event(ScrollEvent {
3392 position: point(px(50.), px(100.)),
3393 delta: ScrollDelta::Pixels(point(px(0.), px(-10000.))),
3394 ..Default::default()
3395 });
3396
3397 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3400 view.clone().into_any_element()
3401 });
3402 assert!(
3403 state.is_following_tail(),
3404 "follow_tail should re-engage after scrolling back to the bottom"
3405 );
3406 }
3407
3408 #[gpui::test]
3411 fn test_follow_tail_reengagement_not_fooled_by_unmeasured_items(cx: &mut TestAppContext) {
3412 let cx = cx.add_empty_window();
3413
3414 let state = ListState::new(20, crate::ListAlignment::Top, px(1000.));
3418
3419 struct TestView(ListState);
3420 impl Render for TestView {
3421 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3422 list(self.0.clone(), |_, _, _| {
3423 div().h(px(50.)).w_full().into_any()
3424 })
3425 .w_full()
3426 .h_full()
3427 }
3428 }
3429
3430 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3431
3432 state.set_follow_mode(FollowMode::Tail);
3433
3434 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3435 view.clone().into_any_element()
3436 });
3437 assert!(state.is_following_tail());
3438
3439 cx.simulate_event(ScrollEvent {
3443 position: point(px(50.), px(100.)),
3444 delta: ScrollDelta::Pixels(point(px(0.), px(200.))),
3445 ..Default::default()
3446 });
3447 assert!(!state.is_following_tail());
3448
3449 state.remeasure_items(19..20);
3453
3454 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3460 view.clone().into_any_element()
3461 });
3462 assert!(
3463 !state.is_following_tail(),
3464 "follow_tail should not falsely re-engage due to an unmeasured item \
3465 reducing items.summary().height"
3466 );
3467 }
3468
3469 #[gpui::test]
3470 fn test_follow_tail_reengages_after_scrollbar_disengagement(cx: &mut TestAppContext) {
3471 let cx = cx.add_empty_window();
3472
3473 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3475
3476 struct TestView(ListState);
3477 impl Render for TestView {
3478 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3479 list(self.0.clone(), |_, _, _| {
3480 div().h(px(50.)).w_full().into_any()
3481 })
3482 .w_full()
3483 .h_full()
3484 }
3485 }
3486
3487 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3488
3489 state.set_follow_mode(FollowMode::Tail);
3490 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3491 view.clone().into_any_element()
3492 });
3493 assert!(state.is_following_tail());
3494
3495 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
3497 assert!(!state.is_following_tail());
3498
3499 state.set_offset_from_scrollbar(point(px(0.), px(-300.)));
3502 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3503 view.into_any_element()
3504 });
3505 assert!(
3506 state.is_following_tail(),
3507 "follow_tail should re-engage after scrolling back to the bottom via the scrollbar"
3508 );
3509 }
3510
3511 #[gpui::test]
3512 fn test_follow_tail_reengages_after_scrollbar_drag_to_bottom_while_growing(
3513 cx: &mut TestAppContext,
3514 ) {
3515 let cx = cx.add_empty_window();
3516
3517 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3518
3519 struct TestView(ListState);
3520 impl Render for TestView {
3521 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3522 list(self.0.clone(), |_, _, _| {
3523 div().h(px(50.)).w_full().into_any()
3524 })
3525 .w_full()
3526 .h_full()
3527 }
3528 }
3529
3530 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3531
3532 state.set_follow_mode(FollowMode::Tail);
3533 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3534 view.clone().into_any_element()
3535 });
3536 assert!(state.is_following_tail());
3537
3538 state.scrollbar_drag_started();
3539
3540 state.splice(10..10, 10);
3541 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3542 view.clone().into_any_element()
3543 });
3544
3545 state.set_offset_from_scrollbar(point(px(0.), px(-300.)));
3546 state.scrollbar_drag_ended();
3547
3548 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3549 view.into_any_element()
3550 });
3551
3552 assert!(
3553 state.is_following_tail(),
3554 "follow_tail should re-engage when the user drags the scrollbar to \
3555 the bottom of its track, even when content has grown during the drag \
3556 (so frozen_bottom < live_bottom)"
3557 );
3558 }
3559
3560 fn draw_list(
3561 state: &ListState,
3562 viewport_height: f32,
3563 render_item: impl Fn(usize) -> AnyElement + 'static,
3564 cx: &mut VisualTestContext,
3565 ) {
3566 cx.draw(
3567 point(px(0.), px(0.)),
3568 size(px(100.), px(viewport_height)),
3569 |_, cx| {
3570 cx.new(|_| TestView {
3571 state: state.clone(),
3572 render_item: Rc::new(render_item),
3573 })
3574 .into_any_element()
3575 },
3576 );
3577 }
3578
3579 fn painted_item(
3580 index: usize,
3581 height: f32,
3582 painted: &Rc<RefCell<Vec<(usize, Pixels, Pixels)>>>,
3583 ) -> AnyElement {
3584 let painted = painted.clone();
3585 canvas(
3586 |_, _, _| {},
3587 move |bounds, _, _, _| {
3588 painted
3589 .borrow_mut()
3590 .push((index, bounds.top(), bounds.bottom()));
3591 },
3592 )
3593 .h(px(height))
3594 .w_full()
3595 .into_any()
3596 }
3597
3598 fn item_readiness(state: &ListState, ready: bool) -> Rc<Cell<bool>> {
3599 let ready = Rc::new(Cell::new(ready));
3600 state.set_item_measurement_readiness({
3601 let ready = ready.clone();
3602 move |_, _| ready.get()
3603 });
3604 ready
3605 }
3606
3607 fn list_offset(item_ix: usize, offset: f32) -> ListOffset {
3608 ListOffset {
3609 item_ix,
3610 offset_in_item: px(offset),
3611 }
3612 }
3613
3614 struct TestView {
3615 state: ListState,
3616 render_item: Rc<dyn Fn(usize) -> AnyElement>,
3617 }
3618
3619 impl Render for TestView {
3620 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3621 let render_item = self.render_item.clone();
3622 list(self.state.clone(), move |index, _, _| render_item(index))
3623 .w_full()
3624 .h_full()
3625 }
3626 }
3627
3628 struct TestListView(ListState);
3629 impl Render for TestListView {
3630 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3631 list(self.0.clone(), |_, _, _| {
3632 div().h(px(20.)).w_full().into_any()
3633 })
3634 .w_full()
3635 .h_full()
3636 }
3637 }
3638}