1use crate::{
11 AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
12 FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,
13 Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, 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 logical_scroll_top: Option<ListOffset>,
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 pending_scroll: Option<PendingScrollFraction>,
75 follow_state: FollowState,
76}
77
78struct PendingScrollFraction {
81 item_ix: usize,
83 fraction: f32,
85}
86
87#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
89pub enum FollowMode {
90 #[default]
92 Normal,
93 Tail,
95}
96
97#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
98enum FollowState {
99 #[default]
100 Normal,
101 Tail {
102 is_following: bool,
103 },
104}
105
106impl FollowState {
107 fn is_following(&self) -> bool {
108 matches!(self, FollowState::Tail { is_following: true })
109 }
110
111 fn has_stopped_following(&self) -> bool {
112 matches!(
113 self,
114 FollowState::Tail {
115 is_following: false
116 }
117 )
118 }
119
120 fn start_following(&mut self) {
121 if let FollowState::Tail {
122 is_following: false,
123 } = self
124 {
125 *self = FollowState::Tail { is_following: true };
126 }
127 }
128
129 fn stop_following(&mut self) {
130 if let FollowState::Tail { is_following: true } = self {
131 *self = FollowState::Tail {
132 is_following: false,
133 };
134 }
135 }
136}
137
138#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140pub enum ListAlignment {
141 Top,
143 Bottom,
145}
146
147pub struct ListScrollEvent {
149 pub visible_range: Range<usize>,
151
152 pub count: usize,
154
155 pub is_scrolled: bool,
157
158 pub is_following_tail: bool,
160}
161
162#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
164pub enum ListSizingBehavior {
165 Infer,
167 #[default]
169 Auto,
170}
171
172#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
174pub enum ListMeasuringBehavior {
175 Measure(bool),
178 #[default]
180 Visible,
181}
182
183impl ListMeasuringBehavior {
184 fn reset(&mut self) {
185 match self {
186 ListMeasuringBehavior::Measure(has_measured) => *has_measured = false,
187 ListMeasuringBehavior::Visible => {}
188 }
189 }
190}
191
192#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
194pub enum ListHorizontalSizingBehavior {
195 #[default]
197 FitList,
198 Unconstrained,
200}
201
202struct LayoutItemsResponse {
203 max_item_width: Pixels,
204 scroll_top: ListOffset,
205 item_layouts: VecDeque<ItemLayout>,
206}
207
208struct ItemLayout {
209 index: usize,
210 element: AnyElement,
211 size: Size<Pixels>,
212}
213
214pub struct ListPrepaintState {
216 hitbox: Hitbox,
217 layout: LayoutItemsResponse,
218}
219
220#[derive(Clone)]
221enum ListItem {
222 Unmeasured {
223 size_hint: Option<Size<Pixels>>,
224 focus_handle: Option<FocusHandle>,
225 },
226 Measured {
227 size: Size<Pixels>,
228 focus_handle: Option<FocusHandle>,
229 },
230}
231
232impl ListItem {
233 fn size(&self) -> Option<Size<Pixels>> {
234 if let ListItem::Measured { size, .. } = self {
235 Some(*size)
236 } else {
237 None
238 }
239 }
240
241 fn size_hint(&self) -> Option<Size<Pixels>> {
242 match self {
243 ListItem::Measured { size, .. } => Some(*size),
244 ListItem::Unmeasured { size_hint, .. } => *size_hint,
245 }
246 }
247
248 fn focus_handle(&self) -> Option<FocusHandle> {
249 match self {
250 ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => {
251 focus_handle.clone()
252 }
253 }
254 }
255
256 fn contains_focused(&self, window: &Window, cx: &App) -> bool {
257 match self {
258 ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => {
259 focus_handle
260 .as_ref()
261 .is_some_and(|handle| handle.contains_focused(window, cx))
262 }
263 }
264 }
265}
266
267#[derive(Clone, Debug, Default, PartialEq)]
268struct ListItemSummary {
269 count: usize,
270 rendered_count: usize,
271 unrendered_count: usize,
272 height: Pixels,
273 has_focus_handles: bool,
274 has_unknown_height: bool,
275}
276
277#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
278struct Count(usize);
279
280#[derive(Clone, Debug, Default)]
281struct Height(Pixels);
282
283impl ListState {
284 pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self {
291 let this = Self(Rc::new(RefCell::new(StateInner {
292 last_layout_bounds: None,
293 last_padding: None,
294 items: SumTree::default(),
295 logical_scroll_top: None,
296 alignment,
297 overdraw,
298 scroll_handler: None,
299 reset: false,
300 scrollbar_drag_start_height: None,
301 measuring_behavior: ListMeasuringBehavior::default(),
302 pending_scroll: None,
303 follow_state: FollowState::default(),
304 })));
305 this.splice(0..0, item_count);
306 this
307 }
308
309 pub fn measure_all(self) -> Self {
313 self.0.borrow_mut().measuring_behavior = ListMeasuringBehavior::Measure(false);
314 self
315 }
316
317 pub fn reset(&self, element_count: usize) {
321 let old_count = {
322 let state = &mut *self.0.borrow_mut();
323 state.reset = true;
324 state.measuring_behavior.reset();
325 state.logical_scroll_top = None;
326 state.scrollbar_drag_start_height = None;
327 state.items.summary().count
328 };
329
330 self.splice(0..old_count, element_count);
331 }
332
333 pub fn remeasure(&self) {
338 let count = self.item_count();
339 self.remeasure_items(0..count);
340 }
341
342 pub fn remeasure_items(&self, range: Range<usize>) {
350 let state = &mut *self.0.borrow_mut();
351
352 if let Some(scroll_top) = state.logical_scroll_top {
357 if range.contains(&scroll_top.item_ix) {
358 let mut cursor = state.items.cursor::<Count>(());
359 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
360
361 if let Some(item) = cursor.item() {
362 if let Some(size) = item.size() {
363 let fraction = if size.height.0 > 0.0 {
364 (scroll_top.offset_in_item.0 / size.height.0).clamp(0.0, 1.0)
365 } else {
366 0.0
367 };
368
369 state.pending_scroll = Some(PendingScrollFraction {
370 item_ix: scroll_top.item_ix,
371 fraction,
372 });
373 }
374 }
375 }
376 }
377
378 let new_items = {
381 let mut cursor = state.items.cursor::<Count>(());
382 let mut new_items = cursor.slice(&Count(range.start), Bias::Right);
383 let invalidated = cursor.slice(&Count(range.end), Bias::Right);
384 new_items.extend(
385 invalidated.iter().map(|item| ListItem::Unmeasured {
386 size_hint: item.size_hint(),
387 focus_handle: item.focus_handle(),
388 }),
389 (),
390 );
391 new_items.append(cursor.suffix(), ());
392 new_items
393 };
394 state.items = new_items;
395 state.measuring_behavior.reset();
396 }
397
398 pub fn item_count(&self) -> usize {
400 self.0.borrow().items.summary().count
401 }
402
403 pub fn is_scrolled_to_end(&self) -> Option<bool> {
406 let state = self.0.borrow();
407 let bounds = state.last_layout_bounds?;
408 let summary = state.items.summary();
409 if summary.has_unknown_height {
410 return None;
411 }
412 let padding = state.last_padding.unwrap_or_default();
413 let content_height = summary.height + padding.top + padding.bottom;
414 let scroll_max = (content_height - bounds.size.height).max(px(0.));
415 if scroll_max <= px(0.) {
416 return None;
417 }
418 let scroll_top = state.scroll_top(&state.logical_scroll_top());
419 Some(scroll_top >= scroll_max)
420 }
421
422 pub fn splice(&self, old_range: Range<usize>, count: usize) {
425 self.splice_focusable(old_range, (0..count).map(|_| None))
426 }
427
428 pub fn splice_focusable(
433 &self,
434 old_range: Range<usize>,
435 focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
436 ) {
437 let state = &mut *self.0.borrow_mut();
438
439 let mut old_items = state.items.cursor::<Count>(());
440 let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right);
441 old_items.seek_forward(&Count(old_range.end), Bias::Right);
442
443 let mut spliced_count = 0;
444 new_items.extend(
445 focus_handles.into_iter().map(|focus_handle| {
446 spliced_count += 1;
447 ListItem::Unmeasured {
448 size_hint: None,
449 focus_handle,
450 }
451 }),
452 (),
453 );
454 new_items.append(old_items.suffix(), ());
455 drop(old_items);
456 state.items = new_items;
457
458 if let Some(ListOffset {
459 item_ix,
460 offset_in_item,
461 }) = state.logical_scroll_top.as_mut()
462 {
463 if old_range.contains(item_ix) {
464 *item_ix = old_range.start;
465 *offset_in_item = px(0.);
466 } else if old_range.end <= *item_ix {
467 *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count;
468 }
469 }
470 }
471
472 pub fn set_scroll_handler(
474 &self,
475 handler: impl FnMut(&ListScrollEvent, &mut Window, &mut App) + 'static,
476 ) {
477 self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
478 }
479
480 pub fn logical_scroll_top(&self) -> ListOffset {
482 self.0.borrow().logical_scroll_top()
483 }
484
485 pub fn scroll_by(&self, distance: Pixels) {
487 if distance == px(0.) {
488 return;
489 }
490
491 let current_offset = self.logical_scroll_top();
492 let state = &mut *self.0.borrow_mut();
493
494 if distance < px(0.) {
495 state.follow_state.stop_following();
496 }
497
498 let mut cursor = state.items.cursor::<ListItemSummary>(());
499 cursor.seek(&Count(current_offset.item_ix), Bias::Right);
500
501 let start_pixel_offset = cursor.start().height + current_offset.offset_in_item;
502 let new_pixel_offset = (start_pixel_offset + distance).max(px(0.));
503 if new_pixel_offset > start_pixel_offset {
504 cursor.seek_forward(&Height(new_pixel_offset), Bias::Right);
505 } else {
506 cursor.seek(&Height(new_pixel_offset), Bias::Right);
507 }
508
509 state.logical_scroll_top = Some(ListOffset {
510 item_ix: cursor.start().count,
511 offset_in_item: new_pixel_offset - cursor.start().height,
512 });
513 }
514
515 pub fn scroll_to_end(&self) {
522 let state = &mut *self.0.borrow_mut();
523 let item_count = state.items.summary().count;
524 state.logical_scroll_top = Some(ListOffset {
525 item_ix: item_count,
526 offset_in_item: px(0.),
527 });
528 }
529
530 pub fn set_follow_mode(&self, mode: FollowMode) {
535 let state = &mut *self.0.borrow_mut();
536
537 match mode {
538 FollowMode::Normal => {
539 state.follow_state = FollowState::Normal;
540 }
541 FollowMode::Tail => {
542 state.follow_state = FollowState::Tail { is_following: true };
543 if matches!(mode, FollowMode::Tail) {
544 let item_count = state.items.summary().count;
545 state.logical_scroll_top = Some(ListOffset {
546 item_ix: item_count,
547 offset_in_item: px(0.),
548 });
549 }
550 }
551 }
552 }
553
554 pub fn is_following_tail(&self) -> bool {
557 matches!(
558 self.0.borrow().follow_state,
559 FollowState::Tail { is_following: true }
560 )
561 }
562
563 pub fn scroll_to(&self, mut scroll_top: ListOffset) {
565 let state = &mut *self.0.borrow_mut();
566 let item_count = state.items.summary().count;
567 if scroll_top.item_ix >= item_count {
568 scroll_top.item_ix = item_count;
569 scroll_top.offset_in_item = px(0.);
570 }
571
572 if scroll_top.item_ix < item_count {
573 state.follow_state.stop_following();
574 }
575
576 state.logical_scroll_top = Some(scroll_top);
577 }
578
579 pub fn scroll_to_reveal_item(&self, ix: usize) {
581 let state = &mut *self.0.borrow_mut();
582
583 let mut scroll_top = state.logical_scroll_top();
584 let height = state
585 .last_layout_bounds
586 .map_or(px(0.), |bounds| bounds.size.height);
587 let padding = state.last_padding.unwrap_or_default();
588
589 if ix <= scroll_top.item_ix {
590 scroll_top.item_ix = ix;
591 scroll_top.offset_in_item = px(0.);
592 } else {
593 let mut cursor = state.items.cursor::<ListItemSummary>(());
594 cursor.seek(&Count(ix + 1), Bias::Right);
595 let bottom = cursor.start().height + padding.top;
596 let goal_top = px(0.).max(bottom - height + padding.bottom);
597
598 cursor.seek(&Height(goal_top), Bias::Left);
599 let start_ix = cursor.start().count;
600 let start_item_top = cursor.start().height;
601
602 if start_ix >= scroll_top.item_ix {
603 scroll_top.item_ix = start_ix;
604 scroll_top.offset_in_item = goal_top - start_item_top;
605 }
606 }
607
608 state.logical_scroll_top = Some(scroll_top);
609 }
610
611 pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
614 let state = &*self.0.borrow();
615
616 let bounds = state.last_layout_bounds.unwrap_or_default();
617 let scroll_top = state.logical_scroll_top();
618 if ix < scroll_top.item_ix {
619 return None;
620 }
621
622 let mut cursor = state.items.cursor::<Dimensions<Count, Height>>(());
623 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
624
625 let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item;
626
627 cursor.seek_forward(&Count(ix), Bias::Right);
628 if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
629 let &Dimensions(Count(count), Height(top), _) = cursor.start();
630 if count == ix {
631 let top = bounds.top() + top - scroll_top;
632 return Some(Bounds::from_corners(
633 point(bounds.left(), top),
634 point(bounds.right(), top + size.height),
635 ));
636 }
637 }
638 None
639 }
640
641 pub fn scrollbar_drag_started(&self) {
646 let mut state = self.0.borrow_mut();
647 state.scrollbar_drag_start_height = Some(state.items.summary().height);
648 }
649
650 pub fn scrollbar_drag_ended(&self) {
654 self.0.borrow_mut().scrollbar_drag_start_height.take();
655 }
656
657 pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
659 self.0.borrow_mut().set_offset_from_scrollbar(point);
660 }
661
662 pub fn max_offset_for_scrollbar(&self) -> Point<Pixels> {
665 let state = self.0.borrow();
666 point(Pixels::ZERO, state.max_scroll_offset())
667 }
668
669 pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
671 let state = &self.0.borrow();
672
673 if state.logical_scroll_top.is_none() && state.alignment == ListAlignment::Bottom {
674 return Point::new(px(0.), -state.max_scroll_offset());
675 }
676
677 let logical_scroll_top = state.logical_scroll_top();
678
679 let mut cursor = state.items.cursor::<ListItemSummary>(());
680 let summary: ListItemSummary =
681 cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right);
682 let content_height = state.items.summary().height;
683 let drag_offset =
684 content_height - state.scrollbar_drag_start_height.unwrap_or(content_height);
686 let offset = summary.height + logical_scroll_top.offset_in_item - drag_offset;
687
688 Point::new(px(0.), -offset)
689 }
690
691 pub fn viewport_bounds(&self) -> Bounds<Pixels> {
693 self.0.borrow().last_layout_bounds.unwrap_or_default()
694 }
695}
696
697impl StateInner {
698 fn max_scroll_offset(&self) -> Pixels {
699 let bounds = self.last_layout_bounds.unwrap_or_default();
700 let height = self
701 .scrollbar_drag_start_height
702 .unwrap_or_else(|| self.items.summary().height);
703 (height - bounds.size.height).max(px(0.))
704 }
705
706 fn visible_range(
707 items: &SumTree<ListItem>,
708 height: Pixels,
709 scroll_top: &ListOffset,
710 ) -> Range<usize> {
711 let mut cursor = items.cursor::<ListItemSummary>(());
712 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
713 let start_y = cursor.start().height + scroll_top.offset_in_item;
714 cursor.seek_forward(&Height(start_y + height), Bias::Left);
715 scroll_top.item_ix..cursor.start().count + 1
716 }
717
718 fn scroll(
719 &mut self,
720 scroll_top: &ListOffset,
721 height: Pixels,
722 delta: Point<Pixels>,
723 current_view: EntityId,
724 window: &mut Window,
725 cx: &mut App,
726 ) {
727 if self.reset {
730 return;
731 }
732
733 let padding = self.last_padding.unwrap_or_default();
734 let scroll_max =
735 (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
736 let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
737 .max(px(0.))
738 .min(scroll_max);
739
740 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
741 self.logical_scroll_top = None;
742 } else {
743 let (start, ..) =
744 self.items
745 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
746 let item_ix = start.count;
747 let offset_in_item = new_scroll_top - start.height;
748 self.logical_scroll_top = Some(ListOffset {
749 item_ix,
750 offset_in_item,
751 });
752 }
753
754 if delta.y > px(0.) {
755 self.follow_state.stop_following();
756 }
757
758 if let Some(handler) = self.scroll_handler.as_mut() {
759 let visible_range = Self::visible_range(&self.items, height, scroll_top);
760 handler(
761 &ListScrollEvent {
762 visible_range,
763 count: self.items.summary().count,
764 is_scrolled: self.logical_scroll_top.is_some(),
765 is_following_tail: matches!(
766 self.follow_state,
767 FollowState::Tail { is_following: true }
768 ),
769 },
770 window,
771 cx,
772 );
773 }
774
775 cx.notify(current_view);
776 }
777
778 fn logical_scroll_top(&self) -> ListOffset {
779 self.logical_scroll_top
780 .unwrap_or_else(|| match self.alignment {
781 ListAlignment::Top => ListOffset {
782 item_ix: 0,
783 offset_in_item: px(0.),
784 },
785 ListAlignment::Bottom => ListOffset {
786 item_ix: self.items.summary().count,
787 offset_in_item: px(0.),
788 },
789 })
790 }
791
792 fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
793 let (start, ..) = self.items.find::<ListItemSummary, _>(
794 (),
795 &Count(logical_scroll_top.item_ix),
796 Bias::Right,
797 );
798 start.height + logical_scroll_top.offset_in_item
799 }
800
801 fn layout_all_items(
802 &mut self,
803 available_width: Pixels,
804 render_item: &mut RenderItemFn,
805 window: &mut Window,
806 cx: &mut App,
807 ) {
808 match &mut self.measuring_behavior {
809 ListMeasuringBehavior::Visible => {
810 return;
811 }
812 ListMeasuringBehavior::Measure(has_measured) => {
813 if *has_measured {
814 return;
815 }
816 *has_measured = true;
817 }
818 }
819
820 let mut cursor = self.items.cursor::<Count>(());
821 let available_item_space = size(
822 AvailableSpace::Definite(available_width),
823 AvailableSpace::MinContent,
824 );
825
826 let mut measured_items = Vec::default();
827
828 for (ix, item) in cursor.enumerate() {
829 let size = item.size().unwrap_or_else(|| {
830 let mut element = render_item(ix, window, cx);
831 element.layout_as_root(available_item_space, window, cx)
832 });
833
834 measured_items.push(ListItem::Measured {
835 size,
836 focus_handle: item.focus_handle(),
837 });
838 }
839
840 self.items = SumTree::from_iter(measured_items, ());
841 }
842
843 fn layout_items(
844 &mut self,
845 available_width: Option<Pixels>,
846 available_height: Pixels,
847 padding: &Edges<Pixels>,
848 render_item: &mut RenderItemFn,
849 window: &mut Window,
850 cx: &mut App,
851 ) -> LayoutItemsResponse {
852 let old_items = self.items.clone();
853 let mut measured_items = VecDeque::new();
854 let mut item_layouts = VecDeque::new();
855 let mut rendered_height = padding.top;
856 let mut max_item_width = px(0.);
857 let mut scroll_top = self.logical_scroll_top();
858
859 if self.follow_state.is_following() {
860 scroll_top = ListOffset {
861 item_ix: self.items.summary().count,
862 offset_in_item: px(0.),
863 };
864 self.logical_scroll_top = Some(scroll_top);
865 }
866
867 let mut rendered_focused_item = false;
868
869 let available_item_space = size(
870 available_width.map_or(AvailableSpace::MinContent, |width| {
871 AvailableSpace::Definite(width)
872 }),
873 AvailableSpace::MinContent,
874 );
875
876 let mut cursor = old_items.cursor::<Count>(());
877
878 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
880 for (ix, item) in cursor.by_ref().enumerate() {
881 let visible_height = rendered_height - scroll_top.offset_in_item;
882 if visible_height >= available_height + self.overdraw {
883 break;
884 }
885
886 let mut size = item.size();
888
889 if visible_height < available_height || size.is_none() {
891 let item_index = scroll_top.item_ix + ix;
892 let mut element = render_item(item_index, window, cx);
893 let element_size = element.layout_as_root(available_item_space, window, cx);
894 size = Some(element_size);
895
896 if ix == 0 {
900 if let Some(pending_scroll) = self.pending_scroll.take() {
901 if pending_scroll.item_ix == scroll_top.item_ix {
902 scroll_top.offset_in_item =
903 Pixels(pending_scroll.fraction * element_size.height.0);
904 self.logical_scroll_top = Some(scroll_top);
905 }
906 }
907 }
908
909 if visible_height < available_height {
910 item_layouts.push_back(ItemLayout {
911 index: item_index,
912 element,
913 size: element_size,
914 });
915 if item.contains_focused(window, cx) {
916 rendered_focused_item = true;
917 }
918 }
919 }
920
921 let size = size.unwrap();
922 rendered_height += size.height;
923 max_item_width = max_item_width.max(size.width);
924 measured_items.push_back(ListItem::Measured {
925 size,
926 focus_handle: item.focus_handle(),
927 });
928 }
929 rendered_height += padding.bottom;
930
931 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
933
934 if rendered_height - scroll_top.offset_in_item < available_height {
937 while rendered_height < available_height {
938 cursor.prev();
939 if let Some(item) = cursor.item() {
940 let item_index = cursor.start().0;
941 let mut element = render_item(item_index, window, cx);
942 let element_size = element.layout_as_root(available_item_space, window, cx);
943 let focus_handle = item.focus_handle();
944 rendered_height += element_size.height;
945 measured_items.push_front(ListItem::Measured {
946 size: element_size,
947 focus_handle,
948 });
949 item_layouts.push_front(ItemLayout {
950 index: item_index,
951 element,
952 size: element_size,
953 });
954 if item.contains_focused(window, cx) {
955 rendered_focused_item = true;
956 }
957 } else {
958 break;
959 }
960 }
961
962 scroll_top = ListOffset {
963 item_ix: cursor.start().0,
964 offset_in_item: rendered_height - available_height,
965 };
966
967 match self.alignment {
968 ListAlignment::Top => {
969 scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
970 self.logical_scroll_top = Some(scroll_top);
971 }
972 ListAlignment::Bottom => {
973 scroll_top = ListOffset {
974 item_ix: cursor.start().0,
975 offset_in_item: rendered_height - available_height,
976 };
977 self.logical_scroll_top = None;
978 }
979 };
980 }
981
982 let mut leading_overdraw = scroll_top.offset_in_item;
984 while leading_overdraw < self.overdraw {
985 cursor.prev();
986 if let Some(item) = cursor.item() {
987 let size = if let ListItem::Measured { size, .. } = item {
988 *size
989 } else {
990 let mut element = render_item(cursor.start().0, window, cx);
991 element.layout_as_root(available_item_space, window, cx)
992 };
993
994 leading_overdraw += size.height;
995 measured_items.push_front(ListItem::Measured {
996 size,
997 focus_handle: item.focus_handle(),
998 });
999 } else {
1000 break;
1001 }
1002 }
1003
1004 let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
1005 let mut cursor = old_items.cursor::<Count>(());
1006 let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right);
1007 new_items.extend(measured_items, ());
1008 cursor.seek(&Count(measured_range.end), Bias::Right);
1009 new_items.append(cursor.suffix(), ());
1010 self.items = new_items;
1011
1012 if self.follow_state.has_stopped_following() {
1016 let padding = self.last_padding.unwrap_or_default();
1017 let total_height = self.items.summary().height + padding.top + padding.bottom;
1018 let scroll_offset = self.scroll_top(&scroll_top);
1019 if scroll_offset + available_height >= total_height - px(1.0) {
1020 self.follow_state.start_following();
1021 }
1022 }
1023
1024 if !rendered_focused_item {
1028 let mut cursor = self
1029 .items
1030 .filter::<_, Count>((), |summary| summary.has_focus_handles);
1031 cursor.next();
1032 while let Some(item) = cursor.item() {
1033 if item.contains_focused(window, cx) {
1034 let item_index = cursor.start().0;
1035 let mut element = render_item(cursor.start().0, window, cx);
1036 let size = element.layout_as_root(available_item_space, window, cx);
1037 item_layouts.push_back(ItemLayout {
1038 index: item_index,
1039 element,
1040 size,
1041 });
1042 break;
1043 }
1044 cursor.next();
1045 }
1046 }
1047
1048 LayoutItemsResponse {
1049 max_item_width,
1050 scroll_top,
1051 item_layouts,
1052 }
1053 }
1054
1055 fn prepaint_items(
1056 &mut self,
1057 bounds: Bounds<Pixels>,
1058 padding: Edges<Pixels>,
1059 autoscroll: bool,
1060 render_item: &mut RenderItemFn,
1061 window: &mut Window,
1062 cx: &mut App,
1063 ) -> Result<LayoutItemsResponse, ListOffset> {
1064 window.transact(|window| {
1065 match self.measuring_behavior {
1066 ListMeasuringBehavior::Measure(has_measured) if !has_measured => {
1067 self.layout_all_items(bounds.size.width, render_item, window, cx);
1068 }
1069 _ => {}
1070 }
1071
1072 let mut layout_response = self.layout_items(
1073 Some(bounds.size.width),
1074 bounds.size.height,
1075 &padding,
1076 render_item,
1077 window,
1078 cx,
1079 );
1080
1081 window.take_autoscroll();
1083
1084 if bounds.size.height > padding.top + padding.bottom {
1086 let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
1087 item_origin.y -= layout_response.scroll_top.offset_in_item;
1088 for item in &mut layout_response.item_layouts {
1089 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1090 item.element.prepaint_at(item_origin, window, cx);
1091 });
1092
1093 if let Some(autoscroll_bounds) = window.take_autoscroll()
1094 && autoscroll
1095 {
1096 if autoscroll_bounds.top() < bounds.top() {
1097 return Err(ListOffset {
1098 item_ix: item.index,
1099 offset_in_item: autoscroll_bounds.top() - item_origin.y,
1100 });
1101 } else if autoscroll_bounds.bottom() > bounds.bottom() {
1102 let mut cursor = self.items.cursor::<Count>(());
1103 cursor.seek(&Count(item.index), Bias::Right);
1104 let mut height = bounds.size.height - padding.top - padding.bottom;
1105
1106 height -= autoscroll_bounds.bottom() - item_origin.y;
1108
1109 while height > Pixels::ZERO {
1111 cursor.prev();
1112 let Some(item) = cursor.item() else { break };
1113
1114 let size = item.size().unwrap_or_else(|| {
1115 let mut item = render_item(cursor.start().0, window, cx);
1116 let item_available_size =
1117 size(bounds.size.width.into(), AvailableSpace::MinContent);
1118 item.layout_as_root(item_available_size, window, cx)
1119 });
1120 height -= size.height;
1121 }
1122
1123 return Err(ListOffset {
1124 item_ix: cursor.start().0,
1125 offset_in_item: if height < Pixels::ZERO {
1126 -height
1127 } else {
1128 Pixels::ZERO
1129 },
1130 });
1131 }
1132 }
1133
1134 item_origin.y += item.size.height;
1135 }
1136 } else {
1137 layout_response.item_layouts.clear();
1138 }
1139
1140 Ok(layout_response)
1141 })
1142 }
1143
1144 fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
1147 let Some(bounds) = self.last_layout_bounds else {
1148 return;
1149 };
1150 let height = bounds.size.height;
1151
1152 let padding = self.last_padding.unwrap_or_default();
1153 let content_height = self.items.summary().height;
1154 let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
1155 let drag_offset =
1156 content_height - self.scrollbar_drag_start_height.unwrap_or(content_height);
1158 let new_scroll_top = (point.y - drag_offset).abs().max(px(0.)).min(scroll_max);
1159
1160 self.follow_state.stop_following();
1161
1162 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
1163 self.logical_scroll_top = None;
1164 } else {
1165 let (start, _, _) =
1166 self.items
1167 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
1168
1169 let item_ix = start.count;
1170 let offset_in_item = new_scroll_top - start.height;
1171 self.logical_scroll_top = Some(ListOffset {
1172 item_ix,
1173 offset_in_item,
1174 });
1175 }
1176 }
1177}
1178
1179impl std::fmt::Debug for ListItem {
1180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1181 match self {
1182 Self::Unmeasured { .. } => write!(f, "Unrendered"),
1183 Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
1184 }
1185 }
1186}
1187
1188#[derive(Debug, Clone, Copy, Default)]
1191pub struct ListOffset {
1192 pub item_ix: usize,
1194 pub offset_in_item: Pixels,
1196}
1197
1198impl Element for List {
1199 type RequestLayoutState = ();
1200 type PrepaintState = ListPrepaintState;
1201
1202 fn id(&self) -> Option<crate::ElementId> {
1203 None
1204 }
1205
1206 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1207 None
1208 }
1209
1210 fn request_layout(
1211 &mut self,
1212 _id: Option<&GlobalElementId>,
1213 _inspector_id: Option<&InspectorElementId>,
1214 window: &mut Window,
1215 cx: &mut App,
1216 ) -> (crate::LayoutId, Self::RequestLayoutState) {
1217 let layout_id = match self.sizing_behavior {
1218 ListSizingBehavior::Infer => {
1219 let mut style = Style::default();
1220 style.overflow.y = Overflow::Scroll;
1221 style.refine(&self.style);
1222 window.with_text_style(style.text_style().cloned(), |window| {
1223 let state = &mut *self.state.0.borrow_mut();
1224
1225 let available_height = if let Some(last_bounds) = state.last_layout_bounds {
1226 last_bounds.size.height
1227 } else {
1228 state.overdraw
1231 };
1232 let padding = style.padding.to_pixels(
1233 state.last_layout_bounds.unwrap_or_default().size.into(),
1234 window.rem_size(),
1235 );
1236
1237 let layout_response = state.layout_items(
1238 None,
1239 available_height,
1240 &padding,
1241 &mut self.render_item,
1242 window,
1243 cx,
1244 );
1245 let max_element_width = layout_response.max_item_width;
1246
1247 let summary = state.items.summary();
1248 let total_height = summary.height;
1249
1250 window.request_measured_layout(
1251 style,
1252 move |known_dimensions, available_space, _window, _cx| {
1253 let width =
1254 known_dimensions
1255 .width
1256 .unwrap_or(match available_space.width {
1257 AvailableSpace::Definite(x) => x,
1258 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1259 max_element_width
1260 }
1261 });
1262 let height = match available_space.height {
1263 AvailableSpace::Definite(height) => total_height.min(height),
1264 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1265 total_height
1266 }
1267 };
1268 size(width, height)
1269 },
1270 )
1271 })
1272 }
1273 ListSizingBehavior::Auto => {
1274 let mut style = Style::default();
1275 style.refine(&self.style);
1276 window.with_text_style(style.text_style().cloned(), |window| {
1277 window.request_layout(style, None, cx)
1278 })
1279 }
1280 };
1281 (layout_id, ())
1282 }
1283
1284 fn prepaint(
1285 &mut self,
1286 _id: Option<&GlobalElementId>,
1287 _inspector_id: Option<&InspectorElementId>,
1288 bounds: Bounds<Pixels>,
1289 _: &mut Self::RequestLayoutState,
1290 window: &mut Window,
1291 cx: &mut App,
1292 ) -> ListPrepaintState {
1293 let state = &mut *self.state.0.borrow_mut();
1294 state.reset = false;
1295
1296 let mut style = Style::default();
1297 style.refine(&self.style);
1298
1299 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1300
1301 if state
1303 .last_layout_bounds
1304 .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width)
1305 {
1306 let new_items = SumTree::from_iter(
1307 state.items.iter().map(|item| ListItem::Unmeasured {
1308 size_hint: None,
1309 focus_handle: item.focus_handle(),
1310 }),
1311 (),
1312 );
1313
1314 state.items = new_items;
1315 state.measuring_behavior.reset();
1316 }
1317
1318 let padding = style
1319 .padding
1320 .to_pixels(bounds.size.into(), window.rem_size());
1321 let layout =
1322 match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) {
1323 Ok(layout) => layout,
1324 Err(autoscroll_request) => {
1325 state.logical_scroll_top = Some(autoscroll_request);
1326 state
1327 .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx)
1328 .unwrap()
1329 }
1330 };
1331
1332 state.last_layout_bounds = Some(bounds);
1333 state.last_padding = Some(padding);
1334 ListPrepaintState { hitbox, layout }
1335 }
1336
1337 fn paint(
1338 &mut self,
1339 _id: Option<&GlobalElementId>,
1340 _inspector_id: Option<&InspectorElementId>,
1341 bounds: Bounds<crate::Pixels>,
1342 _: &mut Self::RequestLayoutState,
1343 prepaint: &mut Self::PrepaintState,
1344 window: &mut Window,
1345 cx: &mut App,
1346 ) {
1347 let current_view = window.current_view();
1348 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1349 for item in &mut prepaint.layout.item_layouts {
1350 item.element.paint(window, cx);
1351 }
1352 });
1353
1354 let list_state = self.state.clone();
1355 let height = bounds.size.height;
1356 let scroll_top = prepaint.layout.scroll_top;
1357 let hitbox_id = prepaint.hitbox.id;
1358 let mut accumulated_scroll_delta = ScrollDelta::default();
1359 window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
1360 if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) {
1361 accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
1362 let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
1363 list_state.0.borrow_mut().scroll(
1364 &scroll_top,
1365 height,
1366 pixel_delta,
1367 current_view,
1368 window,
1369 cx,
1370 )
1371 }
1372 });
1373 }
1374}
1375
1376impl IntoElement for List {
1377 type Element = Self;
1378
1379 fn into_element(self) -> Self::Element {
1380 self
1381 }
1382}
1383
1384impl Styled for List {
1385 fn style(&mut self) -> &mut StyleRefinement {
1386 &mut self.style
1387 }
1388}
1389
1390impl sum_tree::Item for ListItem {
1391 type Summary = ListItemSummary;
1392
1393 fn summary(&self, _: ()) -> Self::Summary {
1394 match self {
1395 ListItem::Unmeasured {
1396 size_hint,
1397 focus_handle,
1398 } => ListItemSummary {
1399 count: 1,
1400 rendered_count: 0,
1401 unrendered_count: 1,
1402 height: if let Some(size) = size_hint {
1403 size.height
1404 } else {
1405 px(0.)
1406 },
1407 has_focus_handles: focus_handle.is_some(),
1408 has_unknown_height: size_hint.is_none(),
1409 },
1410 ListItem::Measured {
1411 size, focus_handle, ..
1412 } => ListItemSummary {
1413 count: 1,
1414 rendered_count: 1,
1415 unrendered_count: 0,
1416 height: size.height,
1417 has_focus_handles: focus_handle.is_some(),
1418 has_unknown_height: false,
1419 },
1420 }
1421 }
1422}
1423
1424impl sum_tree::ContextLessSummary for ListItemSummary {
1425 fn zero() -> Self {
1426 Default::default()
1427 }
1428
1429 fn add_summary(&mut self, summary: &Self) {
1430 self.count += summary.count;
1431 self.rendered_count += summary.rendered_count;
1432 self.unrendered_count += summary.unrendered_count;
1433 self.height += summary.height;
1434 self.has_focus_handles |= summary.has_focus_handles;
1435 self.has_unknown_height |= summary.has_unknown_height;
1436 }
1437}
1438
1439impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
1440 fn zero(_cx: ()) -> Self {
1441 Default::default()
1442 }
1443
1444 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1445 self.0 += summary.count;
1446 }
1447}
1448
1449impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
1450 fn zero(_cx: ()) -> Self {
1451 Default::default()
1452 }
1453
1454 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1455 self.0 += summary.height;
1456 }
1457}
1458
1459impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Count {
1460 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1461 self.0.partial_cmp(&other.count).unwrap()
1462 }
1463}
1464
1465impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height {
1466 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1467 self.0.partial_cmp(&other.height).unwrap()
1468 }
1469}
1470
1471#[cfg(test)]
1472mod test {
1473
1474 use gpui::{ScrollDelta, ScrollWheelEvent};
1475 use std::cell::Cell;
1476 use std::rc::Rc;
1477
1478 use crate::{
1479 self as gpui, AppContext, Context, Element, FollowMode, IntoElement, ListState, Render,
1480 Styled, TestAppContext, Window, div, list, point, px, size,
1481 };
1482
1483 #[gpui::test]
1484 fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
1485 let cx = cx.add_empty_window();
1486
1487 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1488
1489 state.scroll_to(gpui::ListOffset {
1491 item_ix: 0,
1492 offset_in_item: px(0.0),
1493 });
1494
1495 struct TestView(ListState);
1496 impl Render for TestView {
1497 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1498 list(self.0.clone(), |_, _, _| {
1499 div().h(px(10.)).w_full().into_any()
1500 })
1501 .w_full()
1502 .h_full()
1503 }
1504 }
1505
1506 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1508 cx.new(|_| TestView(state.clone())).into_any_element()
1509 });
1510
1511 state.reset(5);
1513
1514 cx.simulate_event(ScrollWheelEvent {
1516 position: point(px(1.), px(1.)),
1517 delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
1518 ..Default::default()
1519 });
1520
1521 assert_eq!(state.logical_scroll_top().item_ix, 0);
1523 assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
1524 }
1525
1526 #[gpui::test]
1527 fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) {
1528 let cx = cx.add_empty_window();
1529
1530 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1531
1532 struct TestView(ListState);
1533 impl Render for TestView {
1534 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1535 list(self.0.clone(), |_, _, _| {
1536 div().h(px(20.)).w_full().into_any()
1537 })
1538 .w_full()
1539 .h_full()
1540 }
1541 }
1542
1543 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
1545 cx.new(|_| TestView(state.clone())).into_any_element()
1546 });
1547
1548 state.scroll_by(px(30.));
1550
1551 let offset = state.logical_scroll_top();
1553 assert_eq!(offset.item_ix, 1);
1554 assert_eq!(offset.offset_in_item, px(10.));
1555
1556 state.scroll_by(px(-30.));
1558
1559 let offset = state.logical_scroll_top();
1561 assert_eq!(offset.item_ix, 0);
1562 assert_eq!(offset.offset_in_item, px(0.));
1563
1564 state.scroll_by(px(0.));
1566 let offset = state.logical_scroll_top();
1567 assert_eq!(offset.item_ix, 0);
1568 assert_eq!(offset.offset_in_item, px(0.));
1569 }
1570
1571 #[gpui::test]
1572 fn test_measure_all_after_width_change(cx: &mut TestAppContext) {
1573 let cx = cx.add_empty_window();
1574
1575 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
1576
1577 struct TestView(ListState);
1578 impl Render for TestView {
1579 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1580 list(self.0.clone(), |_, _, _| {
1581 div().h(px(50.)).w_full().into_any()
1582 })
1583 .w_full()
1584 .h_full()
1585 }
1586 }
1587
1588 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
1589
1590 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1593 view.clone().into_any_element()
1594 });
1595 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
1596
1597 cx.draw(point(px(0.), px(0.)), size(px(200.), px(200.)), |_, _| {
1601 view.into_any_element()
1602 });
1603 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
1604 }
1605
1606 #[gpui::test]
1607 fn test_remeasure(cx: &mut TestAppContext) {
1608 let cx = cx.add_empty_window();
1609
1610 let item_height = Rc::new(Cell::new(100usize));
1614 let state = ListState::new(10, crate::ListAlignment::Top, px(10.));
1615
1616 struct TestView {
1617 state: ListState,
1618 item_height: Rc<Cell<usize>>,
1619 }
1620
1621 impl Render for TestView {
1622 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1623 let height = self.item_height.get();
1624 list(self.state.clone(), move |_, _, _| {
1625 div().h(px(height as f32)).w_full().into_any()
1626 })
1627 .w_full()
1628 .h_full()
1629 }
1630 }
1631
1632 let state_clone = state.clone();
1633 let item_height_clone = item_height.clone();
1634 let view = cx.update(|_, cx| {
1635 cx.new(|_| TestView {
1636 state: state_clone,
1637 item_height: item_height_clone,
1638 })
1639 });
1640
1641 state.scroll_to(gpui::ListOffset {
1644 item_ix: 2,
1645 offset_in_item: px(40.),
1646 });
1647
1648 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1649 view.clone().into_any_element()
1650 });
1651
1652 let offset = state.logical_scroll_top();
1653 assert_eq!(offset.item_ix, 2);
1654 assert_eq!(offset.offset_in_item, px(40.));
1655
1656 item_height.set(50);
1661 state.remeasure();
1662
1663 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1664 view.into_any_element()
1665 });
1666
1667 let offset = state.logical_scroll_top();
1668 assert_eq!(offset.item_ix, 2);
1669 assert_eq!(offset.offset_in_item, px(20.));
1670 }
1671
1672 #[gpui::test]
1673 fn test_follow_tail_stays_at_bottom_as_items_grow(cx: &mut TestAppContext) {
1674 let cx = cx.add_empty_window();
1675
1676 let item_height = Rc::new(Cell::new(50usize));
1679 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
1680
1681 struct TestView {
1682 state: ListState,
1683 item_height: Rc<Cell<usize>>,
1684 }
1685 impl Render for TestView {
1686 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1687 let height = self.item_height.get();
1688 list(self.state.clone(), move |_, _, _| {
1689 div().h(px(height as f32)).w_full().into_any()
1690 })
1691 .w_full()
1692 .h_full()
1693 }
1694 }
1695
1696 let state_clone = state.clone();
1697 let item_height_clone = item_height.clone();
1698 let view = cx.update(|_, cx| {
1699 cx.new(|_| TestView {
1700 state: state_clone,
1701 item_height: item_height_clone,
1702 })
1703 });
1704
1705 state.set_follow_mode(FollowMode::Tail);
1706
1707 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1710 view.clone().into_any_element()
1711 });
1712
1713 let offset = state.logical_scroll_top();
1716 assert_eq!(offset.item_ix, 6);
1717 assert_eq!(offset.offset_in_item, px(0.));
1718 assert!(state.is_following_tail());
1719
1720 item_height.set(80);
1723 state.remeasure();
1724
1725 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1726 view.into_any_element()
1727 });
1728
1729 let offset = state.logical_scroll_top();
1736 assert_eq!(offset.item_ix, 7);
1737 assert_eq!(offset.offset_in_item, px(40.));
1738 assert!(state.is_following_tail());
1739 }
1740
1741 #[gpui::test]
1742 fn test_follow_tail_disengages_on_user_scroll(cx: &mut TestAppContext) {
1743 let cx = cx.add_empty_window();
1744
1745 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
1747
1748 struct TestView(ListState);
1749 impl Render for TestView {
1750 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1751 list(self.0.clone(), |_, _, _| {
1752 div().h(px(50.)).w_full().into_any()
1753 })
1754 .w_full()
1755 .h_full()
1756 }
1757 }
1758
1759 state.set_follow_mode(FollowMode::Tail);
1760
1761 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, cx| {
1763 cx.new(|_| TestView(state.clone())).into_any_element()
1764 });
1765 assert!(state.is_following_tail());
1766
1767 cx.simulate_event(ScrollWheelEvent {
1770 position: point(px(50.), px(100.)),
1771 delta: ScrollDelta::Pixels(point(px(0.), px(100.))),
1772 ..Default::default()
1773 });
1774
1775 assert!(
1776 !state.is_following_tail(),
1777 "follow-tail should disengage when the user scrolls toward the start"
1778 );
1779 }
1780
1781 #[gpui::test]
1782 fn test_follow_tail_disengages_on_scrollbar_reposition(cx: &mut TestAppContext) {
1783 let cx = cx.add_empty_window();
1784
1785 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
1787
1788 struct TestView(ListState);
1789 impl Render for TestView {
1790 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1791 list(self.0.clone(), |_, _, _| {
1792 div().h(px(50.)).w_full().into_any()
1793 })
1794 .w_full()
1795 .h_full()
1796 }
1797 }
1798
1799 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
1800
1801 state.set_follow_mode(FollowMode::Tail);
1802
1803 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1805 view.clone().into_any_element()
1806 });
1807 assert!(state.is_following_tail());
1808
1809 state.set_offset_from_scrollbar(point(px(0.), px(150.)));
1812
1813 let offset = state.logical_scroll_top();
1814 assert_eq!(offset.item_ix, 3);
1815 assert_eq!(offset.offset_in_item, px(0.));
1816 assert!(
1817 !state.is_following_tail(),
1818 "follow-tail should disengage when the scrollbar manually repositions the list"
1819 );
1820
1821 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1824 view.into_any_element()
1825 });
1826
1827 let offset = state.logical_scroll_top();
1828 assert_eq!(offset.item_ix, 3);
1829 assert_eq!(offset.offset_in_item, px(0.));
1830 }
1831
1832 #[gpui::test]
1833 fn test_set_follow_tail_snaps_to_bottom(cx: &mut TestAppContext) {
1834 let cx = cx.add_empty_window();
1835
1836 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
1838
1839 struct TestView(ListState);
1840 impl Render for TestView {
1841 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1842 list(self.0.clone(), |_, _, _| {
1843 div().h(px(50.)).w_full().into_any()
1844 })
1845 .w_full()
1846 .h_full()
1847 }
1848 }
1849
1850 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
1851
1852 state.scroll_to(gpui::ListOffset {
1854 item_ix: 3,
1855 offset_in_item: px(0.),
1856 });
1857
1858 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1859 view.clone().into_any_element()
1860 });
1861
1862 let offset = state.logical_scroll_top();
1863 assert_eq!(offset.item_ix, 3);
1864 assert_eq!(offset.offset_in_item, px(0.));
1865 assert!(!state.is_following_tail());
1866
1867 state.set_follow_mode(FollowMode::Tail);
1870
1871 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1872 view.into_any_element()
1873 });
1874
1875 let offset = state.logical_scroll_top();
1878 assert_eq!(offset.item_ix, 6);
1879 assert_eq!(offset.offset_in_item, px(0.));
1880 assert!(state.is_following_tail());
1881 }
1882
1883 #[gpui::test]
1884 fn test_bottom_aligned_scrollbar_offset_at_end(cx: &mut TestAppContext) {
1885 let cx = cx.add_empty_window();
1886
1887 const ITEMS: usize = 10;
1888 const ITEM_SIZE: f32 = 50.0;
1889
1890 let state = ListState::new(
1891 ITEMS,
1892 crate::ListAlignment::Bottom,
1893 px(ITEMS as f32 * ITEM_SIZE),
1894 );
1895
1896 struct TestView(ListState);
1897 impl Render for TestView {
1898 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1899 list(self.0.clone(), |_, _, _| {
1900 div().h(px(ITEM_SIZE)).w_full().into_any()
1901 })
1902 .w_full()
1903 .h_full()
1904 }
1905 }
1906
1907 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
1908 cx.new(|_| TestView(state.clone())).into_any_element()
1909 });
1910
1911 assert_eq!(state.logical_scroll_top().item_ix, ITEMS);
1914
1915 let max_offset = state.max_offset_for_scrollbar();
1916 let scroll_offset = state.scroll_px_offset_for_scrollbar();
1917
1918 assert_eq!(
1919 -scroll_offset.y, max_offset.y,
1920 "scrollbar offset ({}) should equal max offset ({}) when list is pinned to bottom",
1921 -scroll_offset.y, max_offset.y,
1922 );
1923 }
1924
1925 #[gpui::test]
1929 fn test_follow_tail_reengages_when_scrolled_back_to_bottom(cx: &mut TestAppContext) {
1930 let cx = cx.add_empty_window();
1931
1932 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
1934
1935 struct TestView(ListState);
1936 impl Render for TestView {
1937 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1938 list(self.0.clone(), |_, _, _| {
1939 div().h(px(50.)).w_full().into_any()
1940 })
1941 .w_full()
1942 .h_full()
1943 }
1944 }
1945
1946 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
1947
1948 state.set_follow_mode(FollowMode::Tail);
1949
1950 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1951 view.clone().into_any_element()
1952 });
1953 assert!(state.is_following_tail());
1954
1955 cx.simulate_event(ScrollWheelEvent {
1957 position: point(px(50.), px(100.)),
1958 delta: ScrollDelta::Pixels(point(px(0.), px(50.))),
1959 ..Default::default()
1960 });
1961 assert!(!state.is_following_tail());
1962
1963 cx.simulate_event(ScrollWheelEvent {
1965 position: point(px(50.), px(100.)),
1966 delta: ScrollDelta::Pixels(point(px(0.), px(-10000.))),
1967 ..Default::default()
1968 });
1969
1970 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1973 view.clone().into_any_element()
1974 });
1975 assert!(
1976 state.is_following_tail(),
1977 "follow_tail should re-engage after scrolling back to the bottom"
1978 );
1979 }
1980
1981 #[gpui::test]
1984 fn test_follow_tail_reengagement_not_fooled_by_unmeasured_items(cx: &mut TestAppContext) {
1985 let cx = cx.add_empty_window();
1986
1987 let state = ListState::new(20, crate::ListAlignment::Top, px(1000.));
1991
1992 struct TestView(ListState);
1993 impl Render for TestView {
1994 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1995 list(self.0.clone(), |_, _, _| {
1996 div().h(px(50.)).w_full().into_any()
1997 })
1998 .w_full()
1999 .h_full()
2000 }
2001 }
2002
2003 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2004
2005 state.set_follow_mode(FollowMode::Tail);
2006
2007 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2008 view.clone().into_any_element()
2009 });
2010 assert!(state.is_following_tail());
2011
2012 cx.simulate_event(ScrollWheelEvent {
2016 position: point(px(50.), px(100.)),
2017 delta: ScrollDelta::Pixels(point(px(0.), px(200.))),
2018 ..Default::default()
2019 });
2020 assert!(!state.is_following_tail());
2021
2022 state.remeasure_items(19..20);
2026
2027 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2033 view.clone().into_any_element()
2034 });
2035 assert!(
2036 !state.is_following_tail(),
2037 "follow_tail should not falsely re-engage due to an unmeasured item \
2038 reducing items.summary().height"
2039 );
2040 }
2041
2042 #[gpui::test]
2043 fn test_follow_tail_reengages_after_scrollbar_disengagement(cx: &mut TestAppContext) {
2044 let cx = cx.add_empty_window();
2045
2046 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
2048
2049 struct TestView(ListState);
2050 impl Render for TestView {
2051 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2052 list(self.0.clone(), |_, _, _| {
2053 div().h(px(50.)).w_full().into_any()
2054 })
2055 .w_full()
2056 .h_full()
2057 }
2058 }
2059
2060 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2061
2062 state.set_follow_mode(FollowMode::Tail);
2063 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2064 view.clone().into_any_element()
2065 });
2066 assert!(state.is_following_tail());
2067
2068 state.set_offset_from_scrollbar(point(px(0.), px(150.)));
2070 assert!(!state.is_following_tail());
2071
2072 state.set_offset_from_scrollbar(point(px(0.), px(300.)));
2075 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2076 view.into_any_element()
2077 });
2078 assert!(
2079 state.is_following_tail(),
2080 "follow_tail should re-engage after scrolling back to the bottom via the scrollbar"
2081 );
2082 }
2083}