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 open_gpui_collections::VecDeque;
17use open_gpui_refineable::Refineable as _;
18use open_gpui_sum_tree::{Bias, Dimensions, SumTree};
19use std::{cell::RefCell, ops::Range, rc::Rc};
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<PendingScroll>,
75 follow_state: FollowState,
76}
77
78#[derive(Clone)]
85enum PendingScroll {
86 Absolute { item_ix: usize, offset: Pixels },
88 Proportional(PendingScrollFraction),
90}
91
92#[derive(Clone)]
95struct PendingScrollFraction {
96 item_ix: usize,
98 fraction: f32,
100}
101
102enum ScrollAnchor {
105 Absolute,
107 Proportional,
109}
110
111#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
113pub enum FollowMode {
114 #[default]
116 Normal,
117 Tail,
119}
120
121#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
122enum FollowState {
123 #[default]
124 Normal,
125 Tail {
126 is_following: bool,
127 },
128}
129
130impl FollowState {
131 fn is_following(&self) -> bool {
132 matches!(self, FollowState::Tail { is_following: true })
133 }
134
135 fn has_stopped_following(&self) -> bool {
136 matches!(
137 self,
138 FollowState::Tail {
139 is_following: false
140 }
141 )
142 }
143
144 fn start_following(&mut self) {
145 if let FollowState::Tail {
146 is_following: false,
147 } = self
148 {
149 *self = FollowState::Tail { is_following: true };
150 }
151 }
152
153 fn stop_following(&mut self) {
154 if let FollowState::Tail { is_following: true } = self {
155 *self = FollowState::Tail {
156 is_following: false,
157 };
158 }
159 }
160}
161
162#[derive(Clone, Copy, Debug, Eq, PartialEq)]
164pub enum ListAlignment {
165 Top,
167 Bottom,
169}
170
171pub struct ListScrollEvent {
173 pub visible_range: Range<usize>,
175
176 pub count: usize,
178
179 pub is_scrolled: bool,
181
182 pub is_following_tail: bool,
184}
185
186#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
188pub enum ListSizingBehavior {
189 Infer,
191 #[default]
193 Auto,
194}
195
196#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
198pub enum ListMeasuringBehavior {
199 Measure(bool),
202 #[default]
204 Visible,
205}
206
207impl ListMeasuringBehavior {
208 fn reset(&mut self) {
209 match self {
210 ListMeasuringBehavior::Measure(has_measured) => *has_measured = false,
211 ListMeasuringBehavior::Visible => {}
212 }
213 }
214}
215
216#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
218pub enum ListHorizontalSizingBehavior {
219 #[default]
221 FitList,
222 Unconstrained,
224}
225
226struct LayoutItemsResponse {
227 max_item_width: Pixels,
228 scroll_top: ListOffset,
229 item_layouts: VecDeque<ItemLayout>,
230}
231
232struct ItemLayout {
233 index: usize,
234 element: AnyElement,
235 size: Size<Pixels>,
236}
237
238pub struct ListPrepaintState {
240 hitbox: Hitbox,
241 layout: LayoutItemsResponse,
242}
243
244#[derive(Clone)]
245enum ListItem {
246 Unmeasured {
247 size_hint: Option<Size<Pixels>>,
248 focus_handle: Option<FocusHandle>,
249 },
250 Measured {
251 size: Size<Pixels>,
252 focus_handle: Option<FocusHandle>,
253 },
254}
255
256impl ListItem {
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 this = Self(Rc::new(RefCell::new(StateInner {
316 last_layout_bounds: None,
317 last_padding: None,
318 items: SumTree::default(),
319 logical_scroll_top: None,
320 alignment,
321 overdraw,
322 scroll_handler: None,
323 reset: false,
324 scrollbar_drag_start_height: None,
325 measuring_behavior: ListMeasuringBehavior::default(),
326 pending_scroll: None,
327 follow_state: FollowState::default(),
328 })));
329 this.splice(0..0, item_count);
330 this
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 reset(&self, element_count: usize) {
345 let old_count = {
346 let state = &mut *self.0.borrow_mut();
347 state.reset = true;
348 state.measuring_behavior.reset();
349 state.logical_scroll_top = None;
350 state.scrollbar_drag_start_height = None;
351 state.items.summary().count
352 };
353
354 self.splice(0..old_count, element_count);
355 }
356
357 pub fn remeasure(&self) {
362 let count = self.item_count();
363 self.remeasure_items_with_scroll_anchor(0..count, ScrollAnchor::Proportional);
364 }
365
366 pub fn remeasure_items(&self, range: Range<usize>) {
374 self.remeasure_items_with_scroll_anchor(range, ScrollAnchor::Absolute);
375 }
376
377 fn remeasure_items_with_scroll_anchor(&self, range: Range<usize>, scroll_anchor: ScrollAnchor) {
378 let state = &mut *self.0.borrow_mut();
379
380 if let Some(scroll_top) = state.logical_scroll_top {
381 if range.contains(&scroll_top.item_ix) {
382 state.pending_scroll = match scroll_anchor {
383 ScrollAnchor::Absolute => Some(PendingScroll::Absolute {
384 item_ix: scroll_top.item_ix,
385 offset: scroll_top.offset_in_item,
386 }),
387 ScrollAnchor::Proportional => {
388 let mut cursor = state.items.cursor::<Count>(());
393 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
394
395 cursor
396 .item()
397 .and_then(|item| {
398 item.size().map(|size| {
399 let fraction = if size.height.0 > 0.0 {
400 (scroll_top.offset_in_item.0 / size.height.0)
401 .clamp(0.0, 1.0)
402 } else {
403 0.0
404 };
405
406 PendingScroll::Proportional(PendingScrollFraction {
407 item_ix: scroll_top.item_ix,
408 fraction,
409 })
410 })
411 })
412 .or_else(|| state.pending_scroll.clone())
413 }
414 };
415 }
416 }
417
418 let new_items = {
421 let mut cursor = state.items.cursor::<Count>(());
422 let mut new_items = cursor.slice(&Count(range.start), Bias::Right);
423 let invalidated = cursor.slice(&Count(range.end), Bias::Right);
424 new_items.extend(
425 invalidated.iter().map(|item| ListItem::Unmeasured {
426 size_hint: item.size_hint(),
427 focus_handle: item.focus_handle(),
428 }),
429 (),
430 );
431 new_items.append(cursor.suffix(), ());
432 new_items
433 };
434 state.items = new_items;
435 state.measuring_behavior.reset();
436 }
437
438 pub fn item_count(&self) -> usize {
440 self.0.borrow().items.summary().count
441 }
442
443 pub fn is_scrolled_to_end(&self) -> Option<bool> {
446 let state = self.0.borrow();
447 let bounds = state.last_layout_bounds?;
448 let summary = state.items.summary();
449 if summary.has_unknown_height {
450 return None;
451 }
452 let padding = state.last_padding.unwrap_or_default();
453 let content_height = summary.height + padding.top + padding.bottom;
454 let scroll_max = (content_height - bounds.size.height).max(px(0.));
455 if scroll_max <= px(0.) {
456 return None;
457 }
458 let scroll_top = state.scroll_top(&state.logical_scroll_top());
459 Some(scroll_top >= scroll_max)
460 }
461
462 pub fn splice(&self, old_range: Range<usize>, count: usize) {
465 self.splice_focusable(old_range, (0..count).map(|_| None))
466 }
467
468 pub fn splice_focusable(
473 &self,
474 old_range: Range<usize>,
475 focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
476 ) {
477 let state = &mut *self.0.borrow_mut();
478
479 let mut old_items = state.items.cursor::<Count>(());
480 let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right);
481 old_items.seek_forward(&Count(old_range.end), Bias::Right);
482
483 let mut spliced_count = 0;
484 new_items.extend(
485 focus_handles.into_iter().map(|focus_handle| {
486 spliced_count += 1;
487 ListItem::Unmeasured {
488 size_hint: None,
489 focus_handle,
490 }
491 }),
492 (),
493 );
494 new_items.append(old_items.suffix(), ());
495 drop(old_items);
496 state.items = new_items;
497
498 if let Some(ListOffset {
499 item_ix,
500 offset_in_item,
501 }) = state.logical_scroll_top.as_mut()
502 {
503 if old_range.contains(item_ix) {
504 *item_ix = old_range.start;
505 *offset_in_item = px(0.);
506 } else if old_range.end <= *item_ix {
507 *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count;
508 }
509 }
510 }
511
512 pub fn set_scroll_handler(
514 &self,
515 handler: impl FnMut(&ListScrollEvent, &mut Window, &mut App) + 'static,
516 ) {
517 self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
518 }
519
520 pub fn logical_scroll_top(&self) -> ListOffset {
522 self.0.borrow().logical_scroll_top()
523 }
524
525 pub fn scroll_by(&self, distance: Pixels) {
527 if distance == px(0.) {
528 return;
529 }
530
531 let current_offset = self.logical_scroll_top();
532 let state = &mut *self.0.borrow_mut();
533
534 if distance < px(0.) {
535 state.follow_state.stop_following();
536 }
537
538 let mut cursor = state.items.cursor::<ListItemSummary>(());
539 cursor.seek(&Count(current_offset.item_ix), Bias::Right);
540
541 let start_pixel_offset = cursor.start().height + current_offset.offset_in_item;
542 let new_pixel_offset = (start_pixel_offset + distance).max(px(0.));
543 if new_pixel_offset > start_pixel_offset {
544 cursor.seek_forward(&Height(new_pixel_offset), Bias::Right);
545 } else {
546 cursor.seek(&Height(new_pixel_offset), Bias::Right);
547 }
548
549 state.logical_scroll_top = Some(ListOffset {
550 item_ix: cursor.start().count,
551 offset_in_item: new_pixel_offset - cursor.start().height,
552 });
553 }
554
555 pub fn scroll_to_end(&self) {
562 let state = &mut *self.0.borrow_mut();
563 let item_count = state.items.summary().count;
564 state.logical_scroll_top = Some(ListOffset {
565 item_ix: item_count,
566 offset_in_item: px(0.),
567 });
568 }
569
570 pub fn set_follow_mode(&self, mode: FollowMode) {
575 let state = &mut *self.0.borrow_mut();
576
577 match mode {
578 FollowMode::Normal => {
579 state.follow_state = FollowState::Normal;
580 }
581 FollowMode::Tail => {
582 state.follow_state = FollowState::Tail { is_following: true };
583 if matches!(mode, FollowMode::Tail) {
584 let item_count = state.items.summary().count;
585 state.logical_scroll_top = Some(ListOffset {
586 item_ix: item_count,
587 offset_in_item: px(0.),
588 });
589 }
590 }
591 }
592 }
593
594 pub fn is_following_tail(&self) -> bool {
597 matches!(
598 self.0.borrow().follow_state,
599 FollowState::Tail { is_following: true }
600 )
601 }
602
603 pub fn scroll_to(&self, mut scroll_top: ListOffset) {
605 let state = &mut *self.0.borrow_mut();
606 let item_count = state.items.summary().count;
607 if scroll_top.item_ix >= item_count {
608 scroll_top.item_ix = item_count;
609 scroll_top.offset_in_item = px(0.);
610 }
611
612 if scroll_top.item_ix < item_count {
613 state.follow_state.stop_following();
614 }
615
616 state.logical_scroll_top = Some(scroll_top);
617 }
618
619 pub fn scroll_to_reveal_item(&self, ix: usize) {
621 let state = &mut *self.0.borrow_mut();
622
623 let mut scroll_top = state.logical_scroll_top();
624 let height = state
625 .last_layout_bounds
626 .map_or(px(0.), |bounds| bounds.size.height);
627 let padding = state.last_padding.unwrap_or_default();
628
629 if ix <= scroll_top.item_ix {
630 scroll_top.item_ix = ix;
631 scroll_top.offset_in_item = px(0.);
632 } else {
633 let mut cursor = state.items.cursor::<ListItemSummary>(());
634 cursor.seek(&Count(ix + 1), Bias::Right);
635 let bottom = cursor.start().height + padding.top;
636 let goal_top = px(0.).max(bottom - height + padding.bottom);
637
638 cursor.seek(&Height(goal_top), Bias::Left);
639 let start_ix = cursor.start().count;
640 let start_item_top = cursor.start().height;
641
642 if start_ix >= scroll_top.item_ix {
643 scroll_top.item_ix = start_ix;
644 scroll_top.offset_in_item = goal_top - start_item_top;
645 }
646 }
647
648 state.logical_scroll_top = Some(scroll_top);
649 }
650
651 pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
654 let state = &*self.0.borrow();
655
656 let bounds = state.last_layout_bounds.unwrap_or_default();
657 let scroll_top = state.logical_scroll_top();
658 if ix < scroll_top.item_ix {
659 return None;
660 }
661
662 let mut cursor = state.items.cursor::<Dimensions<Count, Height>>(());
663 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
664
665 let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item;
666
667 cursor.seek_forward(&Count(ix), Bias::Right);
668 if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
669 let &Dimensions(Count(count), Height(top), _) = cursor.start();
670 if count == ix {
671 let top = bounds.top() + top - scroll_top;
672 return Some(Bounds::from_corners(
673 point(bounds.left(), top),
674 point(bounds.right(), top + size.height),
675 ));
676 }
677 }
678 None
679 }
680
681 pub fn scrollbar_drag_started(&self) {
686 let mut state = self.0.borrow_mut();
687 state.scrollbar_drag_start_height = Some(state.items.summary().height);
688 }
689
690 pub fn scrollbar_drag_ended(&self) {
694 self.0.borrow_mut().scrollbar_drag_start_height.take();
695 }
696
697 pub fn is_scrollbar_dragging(&self) -> bool {
704 self.0.borrow().scrollbar_drag_start_height.is_some()
705 }
706
707 pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
709 self.0.borrow_mut().set_offset_from_scrollbar(point);
710 }
711
712 pub fn max_offset_for_scrollbar(&self) -> Point<Pixels> {
715 let state = self.0.borrow();
716 point(Pixels::ZERO, state.max_scroll_offset())
717 }
718
719 pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
724 let state = &self.0.borrow();
725
726 if state.logical_scroll_top.is_none() && state.alignment == ListAlignment::Bottom {
727 return Point::new(px(0.), -state.max_scroll_offset());
728 }
729
730 let logical_scroll_top = state.logical_scroll_top();
731
732 let mut cursor = state.items.cursor::<ListItemSummary>(());
733 let summary: ListItemSummary =
734 cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right);
735 let offset = summary.height + logical_scroll_top.offset_in_item;
736
737 Point::new(px(0.), -offset)
738 }
739
740 pub fn viewport_bounds(&self) -> Bounds<Pixels> {
742 self.0.borrow().last_layout_bounds.unwrap_or_default()
743 }
744
745 pub fn item_is_above_viewport(&self, ix: usize) -> Option<bool> {
753 let viewport_bounds = self.0.borrow().last_layout_bounds?;
754
755 let scroll_top = self.logical_scroll_top();
756 if ix < scroll_top.item_ix {
757 return Some(true);
760 }
761
762 let item_bounds = self.bounds_for_item(ix)?;
763 Some(item_bounds.bottom() <= viewport_bounds.top())
764 }
765
766 pub fn item_is_below_viewport(&self, ix: usize) -> Option<bool> {
772 let viewport_bounds = self.0.borrow().last_layout_bounds?;
773
774 let scroll_top = self.logical_scroll_top();
775 if ix < scroll_top.item_ix {
776 return Some(false);
779 }
780
781 let item_bounds = self.bounds_for_item(ix)?;
782 Some(item_bounds.top() >= viewport_bounds.bottom())
783 }
784}
785
786impl StateInner {
787 fn max_scroll_offset(&self) -> Pixels {
788 let bounds = self.last_layout_bounds.unwrap_or_default();
789 let height = self
790 .scrollbar_drag_start_height
791 .unwrap_or_else(|| self.items.summary().height);
792 (height - bounds.size.height).max(px(0.))
793 }
794
795 fn visible_range(
796 items: &SumTree<ListItem>,
797 height: Pixels,
798 scroll_top: &ListOffset,
799 ) -> Range<usize> {
800 let mut cursor = items.cursor::<ListItemSummary>(());
801 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
802 let start_y = cursor.start().height + scroll_top.offset_in_item;
803 cursor.seek_forward(&Height(start_y + height), Bias::Left);
804 scroll_top.item_ix..cursor.start().count + 1
805 }
806
807 fn scroll(
808 &mut self,
809 scroll_top: &ListOffset,
810 height: Pixels,
811 delta: Point<Pixels>,
812 current_view: EntityId,
813 window: &mut Window,
814 cx: &mut App,
815 ) {
816 if self.reset {
819 return;
820 }
821
822 let padding = self.last_padding.unwrap_or_default();
823 let scroll_max =
824 (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
825 let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
826 .max(px(0.))
827 .min(scroll_max);
828
829 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
830 self.logical_scroll_top = None;
831 } else {
832 let (start, ..) =
833 self.items
834 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
835 let item_ix = start.count;
836 let offset_in_item = new_scroll_top - start.height;
837 self.logical_scroll_top = Some(ListOffset {
838 item_ix,
839 offset_in_item,
840 });
841 }
842
843 if delta.y > px(0.) {
844 self.follow_state.stop_following();
845 }
846
847 if let Some(handler) = self.scroll_handler.as_mut() {
848 let visible_range = Self::visible_range(&self.items, height, scroll_top);
849 handler(
850 &ListScrollEvent {
851 visible_range,
852 count: self.items.summary().count,
853 is_scrolled: self.logical_scroll_top.is_some(),
854 is_following_tail: matches!(
855 self.follow_state,
856 FollowState::Tail { is_following: true }
857 ),
858 },
859 window,
860 cx,
861 );
862 }
863
864 cx.notify(current_view);
865 }
866
867 fn logical_scroll_top(&self) -> ListOffset {
868 self.logical_scroll_top
869 .unwrap_or_else(|| match self.alignment {
870 ListAlignment::Top => ListOffset {
871 item_ix: 0,
872 offset_in_item: px(0.),
873 },
874 ListAlignment::Bottom => ListOffset {
875 item_ix: self.items.summary().count,
876 offset_in_item: px(0.),
877 },
878 })
879 }
880
881 fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
882 let (start, ..) = self.items.find::<ListItemSummary, _>(
883 (),
884 &Count(logical_scroll_top.item_ix),
885 Bias::Right,
886 );
887 start.height + logical_scroll_top.offset_in_item
888 }
889
890 fn layout_all_items(
891 &mut self,
892 available_width: Pixels,
893 render_item: &mut RenderItemFn,
894 window: &mut Window,
895 cx: &mut App,
896 ) {
897 match &mut self.measuring_behavior {
898 ListMeasuringBehavior::Visible => {
899 return;
900 }
901 ListMeasuringBehavior::Measure(has_measured) => {
902 if *has_measured {
903 return;
904 }
905 *has_measured = true;
906 }
907 }
908
909 let mut cursor = self.items.cursor::<Count>(());
910 let available_item_space = size(
911 AvailableSpace::Definite(available_width),
912 AvailableSpace::MinContent,
913 );
914
915 let mut measured_items = Vec::default();
916
917 for (ix, item) in cursor.enumerate() {
918 let size = item.size().unwrap_or_else(|| {
919 let mut element = render_item(ix, window, cx);
920 element.layout_as_root(available_item_space, window, cx)
921 });
922
923 measured_items.push(ListItem::Measured {
924 size,
925 focus_handle: item.focus_handle(),
926 });
927 }
928
929 self.items = SumTree::from_iter(measured_items, ());
930 }
931
932 fn layout_items(
933 &mut self,
934 available_width: Option<Pixels>,
935 available_height: Pixels,
936 padding: &Edges<Pixels>,
937 render_item: &mut RenderItemFn,
938 window: &mut Window,
939 cx: &mut App,
940 ) -> LayoutItemsResponse {
941 let old_items = self.items.clone();
942 let mut measured_items = VecDeque::new();
943 let mut item_layouts = VecDeque::new();
944 let mut rendered_height = padding.top;
945 let mut max_item_width = px(0.);
946 let mut scroll_top = self.logical_scroll_top();
947
948 if self.follow_state.is_following() {
949 scroll_top = ListOffset {
950 item_ix: self.items.summary().count,
951 offset_in_item: px(0.),
952 };
953 self.logical_scroll_top = Some(scroll_top);
954 }
955
956 let mut rendered_focused_item = false;
957
958 let available_item_space = size(
959 available_width.map_or(AvailableSpace::MinContent, |width| {
960 AvailableSpace::Definite(width)
961 }),
962 AvailableSpace::MinContent,
963 );
964
965 let mut cursor = old_items.cursor::<Count>(());
966
967 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
969 for (ix, item) in cursor.by_ref().enumerate() {
970 let visible_height = rendered_height - scroll_top.offset_in_item;
971 if visible_height >= available_height + self.overdraw {
972 break;
973 }
974
975 let mut size = item.size();
977
978 if visible_height < available_height || size.is_none() {
980 let item_index = scroll_top.item_ix + ix;
981 let mut element = render_item(item_index, window, cx);
982 let element_size = element.layout_as_root(available_item_space, window, cx);
983 size = Some(element_size);
984
985 if ix == 0 {
988 if let Some(pending_scroll) = self.pending_scroll.take() {
989 match pending_scroll {
990 PendingScroll::Absolute { item_ix, offset }
991 if item_ix == scroll_top.item_ix =>
992 {
993 scroll_top.offset_in_item = offset.min(element_size.height);
994 self.logical_scroll_top = Some(scroll_top);
995 }
996 PendingScroll::Proportional(pending_scroll)
997 if pending_scroll.item_ix == scroll_top.item_ix =>
998 {
999 scroll_top.offset_in_item =
1002 Pixels(pending_scroll.fraction * element_size.height.0);
1003 self.logical_scroll_top = Some(scroll_top);
1004 }
1005 _ => {}
1006 }
1007 }
1008 }
1009
1010 if visible_height < available_height {
1011 item_layouts.push_back(ItemLayout {
1012 index: item_index,
1013 element,
1014 size: element_size,
1015 });
1016 if item.contains_focused(window, cx) {
1017 rendered_focused_item = true;
1018 }
1019 }
1020 }
1021
1022 let size = size.unwrap();
1023 rendered_height += size.height;
1024 max_item_width = max_item_width.max(size.width);
1025 measured_items.push_back(ListItem::Measured {
1026 size,
1027 focus_handle: item.focus_handle(),
1028 });
1029 }
1030 rendered_height += padding.bottom;
1031
1032 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
1034
1035 if rendered_height - scroll_top.offset_in_item < available_height {
1038 while rendered_height < available_height {
1039 cursor.prev();
1040 if let Some(item) = cursor.item() {
1041 let item_index = cursor.start().0;
1042 let mut element = render_item(item_index, window, cx);
1043 let element_size = element.layout_as_root(available_item_space, window, cx);
1044 let focus_handle = item.focus_handle();
1045 rendered_height += element_size.height;
1046 measured_items.push_front(ListItem::Measured {
1047 size: element_size,
1048 focus_handle,
1049 });
1050 item_layouts.push_front(ItemLayout {
1051 index: item_index,
1052 element,
1053 size: element_size,
1054 });
1055 if item.contains_focused(window, cx) {
1056 rendered_focused_item = true;
1057 }
1058 } else {
1059 break;
1060 }
1061 }
1062
1063 scroll_top = ListOffset {
1064 item_ix: cursor.start().0,
1065 offset_in_item: rendered_height - available_height,
1066 };
1067
1068 match self.alignment {
1069 ListAlignment::Top => {
1070 scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
1071 self.logical_scroll_top = Some(scroll_top);
1072 }
1073 ListAlignment::Bottom => {
1074 scroll_top = ListOffset {
1075 item_ix: cursor.start().0,
1076 offset_in_item: rendered_height - available_height,
1077 };
1078 self.logical_scroll_top = None;
1079 }
1080 };
1081 }
1082
1083 let mut leading_overdraw = scroll_top.offset_in_item;
1085 while leading_overdraw < self.overdraw {
1086 cursor.prev();
1087 if let Some(item) = cursor.item() {
1088 let size = if let ListItem::Measured { size, .. } = item {
1089 *size
1090 } else {
1091 let mut element = render_item(cursor.start().0, window, cx);
1092 element.layout_as_root(available_item_space, window, cx)
1093 };
1094
1095 leading_overdraw += size.height;
1096 measured_items.push_front(ListItem::Measured {
1097 size,
1098 focus_handle: item.focus_handle(),
1099 });
1100 } else {
1101 break;
1102 }
1103 }
1104
1105 let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
1106 let mut cursor = old_items.cursor::<Count>(());
1107 let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right);
1108 new_items.extend(measured_items, ());
1109 cursor.seek(&Count(measured_range.end), Bias::Right);
1110 new_items.append(cursor.suffix(), ());
1111 self.items = new_items;
1112
1113 if self.follow_state.has_stopped_following() {
1117 let padding = self.last_padding.unwrap_or_default();
1118 let total_height = self.items.summary().height + padding.top + padding.bottom;
1119 let scroll_offset = self.scroll_top(&scroll_top);
1120 if scroll_offset + available_height >= total_height - px(1.0) {
1121 self.follow_state.start_following();
1122 }
1123 }
1124
1125 if !rendered_focused_item {
1129 let mut cursor = self
1130 .items
1131 .filter::<_, Count>((), |summary| summary.has_focus_handles);
1132 cursor.next();
1133 while let Some(item) = cursor.item() {
1134 if item.contains_focused(window, cx) {
1135 let item_index = cursor.start().0;
1136 let mut element = render_item(cursor.start().0, window, cx);
1137 let size = element.layout_as_root(available_item_space, window, cx);
1138 item_layouts.push_back(ItemLayout {
1139 index: item_index,
1140 element,
1141 size,
1142 });
1143 break;
1144 }
1145 cursor.next();
1146 }
1147 }
1148
1149 LayoutItemsResponse {
1150 max_item_width,
1151 scroll_top,
1152 item_layouts,
1153 }
1154 }
1155
1156 fn prepaint_items(
1157 &mut self,
1158 bounds: Bounds<Pixels>,
1159 padding: Edges<Pixels>,
1160 autoscroll: bool,
1161 render_item: &mut RenderItemFn,
1162 window: &mut Window,
1163 cx: &mut App,
1164 ) -> Result<LayoutItemsResponse, ListOffset> {
1165 window.transact(|window| {
1166 match self.measuring_behavior {
1167 ListMeasuringBehavior::Measure(has_measured) if !has_measured => {
1168 self.layout_all_items(bounds.size.width, render_item, window, cx);
1169 }
1170 _ => {}
1171 }
1172
1173 let mut layout_response = self.layout_items(
1174 Some(bounds.size.width),
1175 bounds.size.height,
1176 &padding,
1177 render_item,
1178 window,
1179 cx,
1180 );
1181
1182 window.take_autoscroll();
1184
1185 if bounds.size.height > padding.top + padding.bottom {
1187 let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
1188 item_origin.y -= layout_response.scroll_top.offset_in_item;
1189 for item in &mut layout_response.item_layouts {
1190 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1191 item.element.prepaint_at(item_origin, window, cx);
1192 });
1193
1194 if let Some(autoscroll_bounds) = window.take_autoscroll()
1195 && autoscroll
1196 {
1197 if autoscroll_bounds.top() < bounds.top() {
1198 return Err(ListOffset {
1199 item_ix: item.index,
1200 offset_in_item: autoscroll_bounds.top() - item_origin.y,
1201 });
1202 } else if autoscroll_bounds.bottom() > bounds.bottom() {
1203 let mut cursor = self.items.cursor::<Count>(());
1204 cursor.seek(&Count(item.index), Bias::Right);
1205 let mut height = bounds.size.height - padding.top - padding.bottom;
1206
1207 height -= autoscroll_bounds.bottom() - item_origin.y;
1209
1210 while height > Pixels::ZERO {
1212 cursor.prev();
1213 let Some(item) = cursor.item() else { break };
1214
1215 let size = item.size().unwrap_or_else(|| {
1216 let mut item = render_item(cursor.start().0, window, cx);
1217 let item_available_size =
1218 size(bounds.size.width.into(), AvailableSpace::MinContent);
1219 item.layout_as_root(item_available_size, window, cx)
1220 });
1221 height -= size.height;
1222 }
1223
1224 return Err(ListOffset {
1225 item_ix: cursor.start().0,
1226 offset_in_item: if height < Pixels::ZERO {
1227 -height
1228 } else {
1229 Pixels::ZERO
1230 },
1231 });
1232 }
1233 }
1234
1235 item_origin.y += item.size.height;
1236 }
1237 } else {
1238 layout_response.item_layouts.clear();
1239 }
1240
1241 Ok(layout_response)
1242 })
1243 }
1244
1245 fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
1248 let Some(bounds) = self.last_layout_bounds else {
1249 return;
1250 };
1251 let height = bounds.size.height;
1252
1253 let padding = self.last_padding.unwrap_or_default();
1254 let content_height = self
1257 .scrollbar_drag_start_height
1258 .unwrap_or_else(|| self.items.summary().height);
1259 let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
1260 let new_scroll_top = (-point.y).max(px(0.)).min(scroll_max);
1261
1262 let dragged_to_end =
1265 scroll_max > px(0.) && new_scroll_top >= (scroll_max - px(1.0)).max(px(0.));
1266 if dragged_to_end && matches!(self.follow_state, FollowState::Tail { .. }) {
1267 self.follow_state = FollowState::Tail { is_following: true };
1268 let item_count = self.items.summary().count;
1269 self.logical_scroll_top = Some(ListOffset {
1270 item_ix: item_count,
1271 offset_in_item: px(0.),
1272 });
1273 return;
1274 }
1275
1276 self.follow_state.stop_following();
1277
1278 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
1279 self.logical_scroll_top = None;
1280 } else {
1281 let (start, _, _) =
1282 self.items
1283 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
1284
1285 let item_ix = start.count;
1286 let offset_in_item = new_scroll_top - start.height;
1287 self.logical_scroll_top = Some(ListOffset {
1288 item_ix,
1289 offset_in_item,
1290 });
1291 }
1292 }
1293}
1294
1295impl std::fmt::Debug for ListItem {
1296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1297 match self {
1298 Self::Unmeasured { .. } => write!(f, "Unrendered"),
1299 Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
1300 }
1301 }
1302}
1303
1304#[derive(Debug, Clone, Copy, Default)]
1307pub struct ListOffset {
1308 pub item_ix: usize,
1310 pub offset_in_item: Pixels,
1312}
1313
1314impl Element for List {
1315 type RequestLayoutState = ();
1316 type PrepaintState = ListPrepaintState;
1317
1318 fn id(&self) -> Option<crate::ElementId> {
1319 None
1320 }
1321
1322 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1323 None
1324 }
1325
1326 fn request_layout(
1327 &mut self,
1328 _id: Option<&GlobalElementId>,
1329 _inspector_id: Option<&InspectorElementId>,
1330 window: &mut Window,
1331 cx: &mut App,
1332 ) -> (crate::LayoutId, Self::RequestLayoutState) {
1333 let layout_id = match self.sizing_behavior {
1334 ListSizingBehavior::Infer => {
1335 let mut style = Style::default();
1336 style.overflow.y = Overflow::Scroll;
1337 style.refine(&self.style);
1338 window.with_text_style(style.text_style().cloned(), |window| {
1339 let state = &mut *self.state.0.borrow_mut();
1340
1341 let available_height = if let Some(last_bounds) = state.last_layout_bounds {
1342 last_bounds.size.height
1343 } else {
1344 state.overdraw
1347 };
1348 let padding = style.padding.to_pixels(
1349 state.last_layout_bounds.unwrap_or_default().size.into(),
1350 window.rem_size(),
1351 );
1352
1353 let layout_response = state.layout_items(
1354 None,
1355 available_height,
1356 &padding,
1357 &mut self.render_item,
1358 window,
1359 cx,
1360 );
1361 let max_element_width = layout_response.max_item_width;
1362
1363 let summary = state.items.summary();
1364 let total_height = summary.height;
1365
1366 window.request_measured_layout(
1367 style,
1368 move |known_dimensions, available_space, _window, _cx| {
1369 let width =
1370 known_dimensions
1371 .width
1372 .unwrap_or(match available_space.width {
1373 AvailableSpace::Definite(x) => x,
1374 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1375 max_element_width
1376 }
1377 });
1378 let height = match available_space.height {
1379 AvailableSpace::Definite(height) => total_height.min(height),
1380 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1381 total_height
1382 }
1383 };
1384 size(width, height)
1385 },
1386 )
1387 })
1388 }
1389 ListSizingBehavior::Auto => {
1390 let mut style = Style::default();
1391 style.refine(&self.style);
1392 window.with_text_style(style.text_style().cloned(), |window| {
1393 window.request_layout(style, None, cx)
1394 })
1395 }
1396 };
1397 (layout_id, ())
1398 }
1399
1400 fn prepaint(
1401 &mut self,
1402 _id: Option<&GlobalElementId>,
1403 _inspector_id: Option<&InspectorElementId>,
1404 bounds: Bounds<Pixels>,
1405 _: &mut Self::RequestLayoutState,
1406 window: &mut Window,
1407 cx: &mut App,
1408 ) -> ListPrepaintState {
1409 let state = &mut *self.state.0.borrow_mut();
1410 state.reset = false;
1411
1412 let mut style = Style::default();
1413 style.refine(&self.style);
1414
1415 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1416
1417 if state
1419 .last_layout_bounds
1420 .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width)
1421 {
1422 let new_items = SumTree::from_iter(
1423 state.items.iter().map(|item| ListItem::Unmeasured {
1424 size_hint: None,
1425 focus_handle: item.focus_handle(),
1426 }),
1427 (),
1428 );
1429
1430 state.items = new_items;
1431 state.measuring_behavior.reset();
1432 }
1433
1434 let padding = style
1435 .padding
1436 .to_pixels(bounds.size.into(), window.rem_size());
1437 let layout =
1438 match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) {
1439 Ok(layout) => layout,
1440 Err(autoscroll_request) => {
1441 state.logical_scroll_top = Some(autoscroll_request);
1442 state
1443 .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx)
1444 .unwrap()
1445 }
1446 };
1447
1448 state.last_layout_bounds = Some(bounds);
1449 state.last_padding = Some(padding);
1450 ListPrepaintState { hitbox, layout }
1451 }
1452
1453 fn paint(
1454 &mut self,
1455 _id: Option<&GlobalElementId>,
1456 _inspector_id: Option<&InspectorElementId>,
1457 bounds: Bounds<crate::Pixels>,
1458 _: &mut Self::RequestLayoutState,
1459 prepaint: &mut Self::PrepaintState,
1460 window: &mut Window,
1461 cx: &mut App,
1462 ) {
1463 let current_view = window.current_view();
1464 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1465 for item in &mut prepaint.layout.item_layouts {
1466 item.element.paint(window, cx);
1467 }
1468 });
1469
1470 let list_state = self.state.clone();
1471 let height = bounds.size.height;
1472 let scroll_top = prepaint.layout.scroll_top;
1473 let hitbox_id = prepaint.hitbox.id;
1474 let mut accumulated_scroll_delta = ScrollDelta::default();
1475 window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
1476 if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) {
1477 accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
1478 let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
1479 list_state.0.borrow_mut().scroll(
1480 &scroll_top,
1481 height,
1482 pixel_delta,
1483 current_view,
1484 window,
1485 cx,
1486 )
1487 }
1488 });
1489 }
1490}
1491
1492impl IntoElement for List {
1493 type Element = Self;
1494
1495 fn into_element(self) -> Self::Element {
1496 self
1497 }
1498}
1499
1500impl Styled for List {
1501 fn style(&mut self) -> &mut StyleRefinement {
1502 &mut self.style
1503 }
1504}
1505
1506impl open_gpui_sum_tree::Item for ListItem {
1507 type Summary = ListItemSummary;
1508
1509 fn summary(&self, _: ()) -> Self::Summary {
1510 match self {
1511 ListItem::Unmeasured {
1512 size_hint,
1513 focus_handle,
1514 } => ListItemSummary {
1515 count: 1,
1516 rendered_count: 0,
1517 unrendered_count: 1,
1518 height: if let Some(size) = size_hint {
1519 size.height
1520 } else {
1521 px(0.)
1522 },
1523 has_focus_handles: focus_handle.is_some(),
1524 has_unknown_height: size_hint.is_none(),
1525 },
1526 ListItem::Measured {
1527 size, focus_handle, ..
1528 } => ListItemSummary {
1529 count: 1,
1530 rendered_count: 1,
1531 unrendered_count: 0,
1532 height: size.height,
1533 has_focus_handles: focus_handle.is_some(),
1534 has_unknown_height: false,
1535 },
1536 }
1537 }
1538}
1539
1540impl open_gpui_sum_tree::ContextLessSummary for ListItemSummary {
1541 fn zero() -> Self {
1542 Default::default()
1543 }
1544
1545 fn add_summary(&mut self, summary: &Self) {
1546 self.count += summary.count;
1547 self.rendered_count += summary.rendered_count;
1548 self.unrendered_count += summary.unrendered_count;
1549 self.height += summary.height;
1550 self.has_focus_handles |= summary.has_focus_handles;
1551 self.has_unknown_height |= summary.has_unknown_height;
1552 }
1553}
1554
1555impl<'a> open_gpui_sum_tree::Dimension<'a, ListItemSummary> for Count {
1556 fn zero(_cx: ()) -> Self {
1557 Default::default()
1558 }
1559
1560 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1561 self.0 += summary.count;
1562 }
1563}
1564
1565impl<'a> open_gpui_sum_tree::Dimension<'a, ListItemSummary> for Height {
1566 fn zero(_cx: ()) -> Self {
1567 Default::default()
1568 }
1569
1570 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1571 self.0 += summary.height;
1572 }
1573}
1574
1575impl open_gpui_sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Count {
1576 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1577 self.0.partial_cmp(&other.count).unwrap()
1578 }
1579}
1580
1581impl open_gpui_sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height {
1582 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1583 self.0.partial_cmp(&other.height).unwrap()
1584 }
1585}
1586
1587#[cfg(test)]
1588mod test {
1589
1590 use open_gpui::{ScrollDelta, ScrollWheelEvent};
1591 use std::cell::Cell;
1592 use std::rc::Rc;
1593
1594 use crate::{
1595 self as gpui, AppContext, Context, Element, FollowMode, IntoElement, ListState, Render,
1596 Styled, TestAppContext, Window, div, list, point, px, size,
1597 };
1598
1599 #[open_gpui::test]
1600 fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
1601 let cx = cx.add_empty_window();
1602
1603 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1604
1605 state.scroll_to(open_gpui::ListOffset {
1607 item_ix: 0,
1608 offset_in_item: px(0.0),
1609 });
1610
1611 struct TestView(ListState);
1612 impl Render for TestView {
1613 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1614 list(self.0.clone(), |_, _, _| {
1615 div().h(px(10.)).w_full().into_any()
1616 })
1617 .w_full()
1618 .h_full()
1619 }
1620 }
1621
1622 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1624 cx.new(|_| TestView(state.clone())).into_any_element()
1625 });
1626
1627 state.reset(5);
1629
1630 cx.simulate_event(ScrollWheelEvent {
1632 position: point(px(1.), px(1.)),
1633 delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
1634 ..Default::default()
1635 });
1636
1637 assert_eq!(state.logical_scroll_top().item_ix, 0);
1639 assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
1640 }
1641
1642 #[open_gpui::test]
1643 fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) {
1644 let cx = cx.add_empty_window();
1645
1646 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1647
1648 struct TestView(ListState);
1649 impl Render for TestView {
1650 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1651 list(self.0.clone(), |_, _, _| {
1652 div().h(px(20.)).w_full().into_any()
1653 })
1654 .w_full()
1655 .h_full()
1656 }
1657 }
1658
1659 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
1661 cx.new(|_| TestView(state.clone())).into_any_element()
1662 });
1663
1664 state.scroll_by(px(30.));
1666
1667 let offset = state.logical_scroll_top();
1669 assert_eq!(offset.item_ix, 1);
1670 assert_eq!(offset.offset_in_item, px(10.));
1671
1672 state.scroll_by(px(-30.));
1674
1675 let offset = state.logical_scroll_top();
1677 assert_eq!(offset.item_ix, 0);
1678 assert_eq!(offset.offset_in_item, px(0.));
1679
1680 state.scroll_by(px(0.));
1682 let offset = state.logical_scroll_top();
1683 assert_eq!(offset.item_ix, 0);
1684 assert_eq!(offset.offset_in_item, px(0.));
1685 }
1686
1687 struct TestListView(ListState);
1688 impl Render for TestListView {
1689 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1690 list(self.0.clone(), |_, _, _| {
1691 div().h(px(20.)).w_full().into_any()
1692 })
1693 .w_full()
1694 .h_full()
1695 }
1696 }
1697
1698 #[open_gpui::test]
1699 fn test_item_viewport_queries_return_none_before_layout(_cx: &mut TestAppContext) {
1700 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1701
1702 assert_eq!(state.item_is_above_viewport(0), None);
1703 assert_eq!(state.item_is_below_viewport(0), None);
1704 }
1705
1706 #[open_gpui::test]
1707 fn test_item_viewport_queries_before_logical_scroll_top(cx: &mut TestAppContext) {
1708 let cx = cx.add_empty_window();
1709
1710 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1711
1712 state.scroll_to(open_gpui::ListOffset {
1713 item_ix: 2,
1714 offset_in_item: px(0.),
1715 });
1716 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1717 cx.new(|_| TestListView(state.clone())).into_any_element()
1718 });
1719
1720 assert_eq!(state.item_is_above_viewport(1), Some(true));
1721 assert_eq!(state.item_is_below_viewport(1), Some(false));
1722 }
1723
1724 #[open_gpui::test]
1725 fn test_item_viewport_queries_measured_item_inside_viewport(cx: &mut TestAppContext) {
1726 let cx = cx.add_empty_window();
1727
1728 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1729
1730 state.scroll_to(open_gpui::ListOffset {
1731 item_ix: 2,
1732 offset_in_item: px(0.),
1733 });
1734 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1735 cx.new(|_| TestListView(state.clone())).into_any_element()
1736 });
1737
1738 assert_eq!(state.item_is_above_viewport(2), Some(false));
1739 assert_eq!(state.item_is_below_viewport(2), Some(false));
1740 }
1741
1742 #[open_gpui::test]
1743 fn test_item_viewport_queries_measured_item_above_viewport(cx: &mut TestAppContext) {
1744 let cx = cx.add_empty_window();
1745
1746 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1747
1748 state.scroll_to(open_gpui::ListOffset {
1749 item_ix: 2,
1750 offset_in_item: px(20.),
1751 });
1752 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1753 cx.new(|_| TestListView(state.clone())).into_any_element()
1754 });
1755
1756 assert_eq!(state.item_is_above_viewport(2), Some(true));
1757 assert_eq!(state.item_is_below_viewport(2), Some(false));
1758 }
1759
1760 #[open_gpui::test]
1761 fn test_item_viewport_queries_measured_item_below_viewport(cx: &mut TestAppContext) {
1762 let cx = cx.add_empty_window();
1763
1764 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1765
1766 state.scroll_to(open_gpui::ListOffset {
1767 item_ix: 2,
1768 offset_in_item: px(0.),
1769 });
1770 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1771 cx.new(|_| TestListView(state.clone())).into_any_element()
1772 });
1773
1774 assert_eq!(state.item_is_above_viewport(3), Some(false));
1775 assert_eq!(state.item_is_below_viewport(3), Some(true));
1776 }
1777
1778 #[open_gpui::test]
1779 fn test_item_viewport_queries_remain_stable_with_zero_height_viewport(cx: &mut TestAppContext) {
1780 let cx = cx.add_empty_window();
1781
1782 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1783
1784 state.scroll_to(open_gpui::ListOffset {
1785 item_ix: 2,
1786 offset_in_item: px(0.),
1787 });
1788 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1789 cx.new(|_| TestListView(state.clone())).into_any_element()
1790 });
1791
1792 assert_eq!(state.item_is_above_viewport(3), Some(false));
1793 assert_eq!(state.item_is_below_viewport(3), Some(true));
1794
1795 cx.draw(point(px(0.), px(0.)), size(px(100.), px(0.)), |_, cx| {
1800 cx.new(|_| TestListView(state.clone())).into_any_element()
1801 });
1802
1803 assert_eq!(state.item_is_above_viewport(1), Some(true));
1804 assert_eq!(state.item_is_below_viewport(1), Some(false));
1805 assert_eq!(state.item_is_above_viewport(3), Some(false));
1806 assert_eq!(state.item_is_below_viewport(3), Some(true));
1807 }
1808
1809 #[open_gpui::test]
1810 fn test_item_viewport_queries_after_scroll_to_end_before_layout(cx: &mut TestAppContext) {
1811 let cx = cx.add_empty_window();
1812
1813 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1814
1815 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1816 cx.new(|_| TestListView(state.clone())).into_any_element()
1817 });
1818
1819 state.scroll_to_end();
1820
1821 assert_eq!(state.logical_scroll_top().item_ix, state.item_count());
1822 assert_eq!(state.item_is_above_viewport(0), Some(true));
1823 assert_eq!(state.item_is_below_viewport(0), Some(false));
1824 }
1825
1826 #[open_gpui::test]
1827 fn test_measure_all_after_width_change(cx: &mut TestAppContext) {
1828 let cx = cx.add_empty_window();
1829
1830 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
1831
1832 struct TestView(ListState);
1833 impl Render for TestView {
1834 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1835 list(self.0.clone(), |_, _, _| {
1836 div().h(px(50.)).w_full().into_any()
1837 })
1838 .w_full()
1839 .h_full()
1840 }
1841 }
1842
1843 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
1844
1845 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1848 view.clone().into_any_element()
1849 });
1850 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
1851
1852 cx.draw(point(px(0.), px(0.)), size(px(200.), px(200.)), |_, _| {
1856 view.into_any_element()
1857 });
1858 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
1859 }
1860
1861 #[open_gpui::test]
1862 fn test_remeasure(cx: &mut TestAppContext) {
1863 let cx = cx.add_empty_window();
1864
1865 let item_height = Rc::new(Cell::new(100usize));
1869 let state = ListState::new(10, crate::ListAlignment::Top, px(10.));
1870
1871 struct TestView {
1872 state: ListState,
1873 item_height: Rc<Cell<usize>>,
1874 }
1875
1876 impl Render for TestView {
1877 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1878 let height = self.item_height.get();
1879 list(self.state.clone(), move |_, _, _| {
1880 div().h(px(height as f32)).w_full().into_any()
1881 })
1882 .w_full()
1883 .h_full()
1884 }
1885 }
1886
1887 let state_clone = state.clone();
1888 let item_height_clone = item_height.clone();
1889 let view = cx.update(|_, cx| {
1890 cx.new(|_| TestView {
1891 state: state_clone,
1892 item_height: item_height_clone,
1893 })
1894 });
1895
1896 state.scroll_to(open_gpui::ListOffset {
1899 item_ix: 2,
1900 offset_in_item: px(40.),
1901 });
1902
1903 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1904 view.clone().into_any_element()
1905 });
1906
1907 let offset = state.logical_scroll_top();
1908 assert_eq!(offset.item_ix, 2);
1909 assert_eq!(offset.offset_in_item, px(40.));
1910
1911 item_height.set(50);
1916 state.remeasure();
1917
1918 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1919 view.into_any_element()
1920 });
1921
1922 let offset = state.logical_scroll_top();
1923 assert_eq!(offset.item_ix, 2);
1924 assert_eq!(offset.offset_in_item, px(20.));
1925 }
1926
1927 #[open_gpui::test]
1928 fn test_remeasure_item_preserves_scroll_offset(cx: &mut TestAppContext) {
1929 let cx = cx.add_empty_window();
1930
1931 let item_height = Rc::new(Cell::new(100usize));
1932 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
1933
1934 struct TestView {
1935 state: ListState,
1936 item_height: Rc<Cell<usize>>,
1937 }
1938
1939 impl Render for TestView {
1940 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1941 let height = self.item_height.get();
1942 list(self.state.clone(), move |index, _, _| {
1943 let height = if index == 5 { height } else { 100 };
1944 div().h(px(height as f32)).w_full().into_any()
1945 })
1946 .w_full()
1947 .h_full()
1948 }
1949 }
1950
1951 let state_clone = state.clone();
1952 let item_height_clone = item_height.clone();
1953 let view = cx.update(|_, cx| {
1954 cx.new(|_| TestView {
1955 state: state_clone,
1956 item_height: item_height_clone,
1957 })
1958 });
1959
1960 state.scroll_to(open_gpui::ListOffset {
1961 item_ix: 5,
1962 offset_in_item: px(40.),
1963 });
1964
1965 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1966 view.clone().into_any_element()
1967 });
1968
1969 item_height.set(200);
1970 state.remeasure_items(5..6);
1971
1972 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1973 view.into_any_element()
1974 });
1975
1976 let offset = state.logical_scroll_top();
1977 assert_eq!(offset.item_ix, 5);
1978 assert_eq!(offset.offset_in_item, px(40.));
1979 }
1980
1981 #[open_gpui::test]
1982 fn test_follow_tail_stays_at_bottom_as_items_grow(cx: &mut TestAppContext) {
1983 let cx = cx.add_empty_window();
1984
1985 let item_height = Rc::new(Cell::new(50usize));
1988 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
1989
1990 struct TestView {
1991 state: ListState,
1992 item_height: Rc<Cell<usize>>,
1993 }
1994 impl Render for TestView {
1995 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1996 let height = self.item_height.get();
1997 list(self.state.clone(), move |_, _, _| {
1998 div().h(px(height as f32)).w_full().into_any()
1999 })
2000 .w_full()
2001 .h_full()
2002 }
2003 }
2004
2005 let state_clone = state.clone();
2006 let item_height_clone = item_height.clone();
2007 let view = cx.update(|_, cx| {
2008 cx.new(|_| TestView {
2009 state: state_clone,
2010 item_height: item_height_clone,
2011 })
2012 });
2013
2014 state.set_follow_mode(FollowMode::Tail);
2015
2016 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2019 view.clone().into_any_element()
2020 });
2021
2022 let offset = state.logical_scroll_top();
2025 assert_eq!(offset.item_ix, 6);
2026 assert_eq!(offset.offset_in_item, px(0.));
2027 assert!(state.is_following_tail());
2028
2029 item_height.set(80);
2032 state.remeasure();
2033
2034 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2035 view.into_any_element()
2036 });
2037
2038 let offset = state.logical_scroll_top();
2045 assert_eq!(offset.item_ix, 7);
2046 assert_eq!(offset.offset_in_item, px(40.));
2047 assert!(state.is_following_tail());
2048 }
2049
2050 #[open_gpui::test]
2051 fn test_follow_tail_disengages_on_user_scroll(cx: &mut TestAppContext) {
2052 let cx = cx.add_empty_window();
2053
2054 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2056
2057 struct TestView(ListState);
2058 impl Render for TestView {
2059 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2060 list(self.0.clone(), |_, _, _| {
2061 div().h(px(50.)).w_full().into_any()
2062 })
2063 .w_full()
2064 .h_full()
2065 }
2066 }
2067
2068 state.set_follow_mode(FollowMode::Tail);
2069
2070 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, cx| {
2072 cx.new(|_| TestView(state.clone())).into_any_element()
2073 });
2074 assert!(state.is_following_tail());
2075
2076 cx.simulate_event(ScrollWheelEvent {
2079 position: point(px(50.), px(100.)),
2080 delta: ScrollDelta::Pixels(point(px(0.), px(100.))),
2081 ..Default::default()
2082 });
2083
2084 assert!(
2085 !state.is_following_tail(),
2086 "follow-tail should disengage when the user scrolls toward the start"
2087 );
2088 }
2089
2090 #[open_gpui::test]
2091 fn test_follow_tail_disengages_on_scrollbar_reposition(cx: &mut TestAppContext) {
2092 let cx = cx.add_empty_window();
2093
2094 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
2096
2097 struct TestView(ListState);
2098 impl Render for TestView {
2099 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2100 list(self.0.clone(), |_, _, _| {
2101 div().h(px(50.)).w_full().into_any()
2102 })
2103 .w_full()
2104 .h_full()
2105 }
2106 }
2107
2108 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2109
2110 state.set_follow_mode(FollowMode::Tail);
2111
2112 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2114 view.clone().into_any_element()
2115 });
2116 assert!(state.is_following_tail());
2117
2118 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
2120
2121 let offset = state.logical_scroll_top();
2122 assert_eq!(offset.item_ix, 3);
2123 assert_eq!(offset.offset_in_item, px(0.));
2124 assert!(
2125 !state.is_following_tail(),
2126 "follow-tail should disengage when the scrollbar manually repositions the list"
2127 );
2128
2129 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2132 view.into_any_element()
2133 });
2134
2135 let offset = state.logical_scroll_top();
2136 assert_eq!(offset.item_ix, 3);
2137 assert_eq!(offset.offset_in_item, px(0.));
2138 }
2139
2140 #[open_gpui::test]
2141 fn test_scrollbar_drag_with_growing_content(cx: &mut TestAppContext) {
2142 let cx = cx.add_empty_window();
2143
2144 let last_item_height = Rc::new(Cell::new(50usize));
2145 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
2146
2147 struct TestView {
2148 state: ListState,
2149 last_item_height: Rc<Cell<usize>>,
2150 }
2151 impl Render for TestView {
2152 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2153 let last_item_height = self.last_item_height.clone();
2154 list(self.state.clone(), move |index, _, _| {
2155 let height = if index == 9 {
2156 last_item_height.get()
2157 } else {
2158 50
2159 };
2160 div().h(px(height as f32)).w_full().into_any()
2161 })
2162 .w_full()
2163 .h_full()
2164 }
2165 }
2166
2167 let view = cx.update(|_, cx| {
2168 cx.new(|_| TestView {
2169 state: state.clone(),
2170 last_item_height: last_item_height.clone(),
2171 })
2172 });
2173
2174 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2175 view.clone().into_any_element()
2176 });
2177
2178 state.scrollbar_drag_started();
2179
2180 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
2181 let scrollbar_offset_before_growth = state.scroll_px_offset_for_scrollbar();
2182
2183 let offset = state.logical_scroll_top();
2184 assert_eq!(offset.item_ix, 3);
2185 assert_eq!(offset.offset_in_item, px(0.));
2186
2187 last_item_height.set(550);
2188 state.remeasure_items(9..10);
2189 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2190 view.clone().into_any_element()
2191 });
2192
2193 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
2194 assert_eq!(
2195 state.scroll_px_offset_for_scrollbar(),
2196 scrollbar_offset_before_growth
2197 );
2198
2199 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
2200 let offset = state.logical_scroll_top();
2201 assert_eq!(offset.item_ix, 3);
2202 assert_eq!(offset.offset_in_item, px(0.));
2203 }
2204
2205 #[open_gpui::test]
2206 fn test_set_follow_tail_snaps_to_bottom(cx: &mut TestAppContext) {
2207 let cx = cx.add_empty_window();
2208
2209 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2211
2212 struct TestView(ListState);
2213 impl Render for TestView {
2214 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2215 list(self.0.clone(), |_, _, _| {
2216 div().h(px(50.)).w_full().into_any()
2217 })
2218 .w_full()
2219 .h_full()
2220 }
2221 }
2222
2223 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2224
2225 state.scroll_to(open_gpui::ListOffset {
2227 item_ix: 3,
2228 offset_in_item: px(0.),
2229 });
2230
2231 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2232 view.clone().into_any_element()
2233 });
2234
2235 let offset = state.logical_scroll_top();
2236 assert_eq!(offset.item_ix, 3);
2237 assert_eq!(offset.offset_in_item, px(0.));
2238 assert!(!state.is_following_tail());
2239
2240 state.set_follow_mode(FollowMode::Tail);
2243
2244 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2245 view.into_any_element()
2246 });
2247
2248 let offset = state.logical_scroll_top();
2251 assert_eq!(offset.item_ix, 6);
2252 assert_eq!(offset.offset_in_item, px(0.));
2253 assert!(state.is_following_tail());
2254 }
2255
2256 #[open_gpui::test]
2257 fn test_bottom_aligned_scrollbar_offset_at_end(cx: &mut TestAppContext) {
2258 let cx = cx.add_empty_window();
2259
2260 const ITEMS: usize = 10;
2261 const ITEM_SIZE: f32 = 50.0;
2262
2263 let state = ListState::new(
2264 ITEMS,
2265 crate::ListAlignment::Bottom,
2266 px(ITEMS as f32 * ITEM_SIZE),
2267 );
2268
2269 struct TestView(ListState);
2270 impl Render for TestView {
2271 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2272 list(self.0.clone(), |_, _, _| {
2273 div().h(px(ITEM_SIZE)).w_full().into_any()
2274 })
2275 .w_full()
2276 .h_full()
2277 }
2278 }
2279
2280 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
2281 cx.new(|_| TestView(state.clone())).into_any_element()
2282 });
2283
2284 assert_eq!(state.logical_scroll_top().item_ix, ITEMS);
2287
2288 let max_offset = state.max_offset_for_scrollbar();
2289 let scroll_offset = state.scroll_px_offset_for_scrollbar();
2290
2291 assert_eq!(
2292 -scroll_offset.y, max_offset.y,
2293 "scrollbar offset ({}) should equal max offset ({}) when list is pinned to bottom",
2294 -scroll_offset.y, max_offset.y,
2295 );
2296 }
2297
2298 #[open_gpui::test]
2302 fn test_follow_tail_reengages_when_scrolled_back_to_bottom(cx: &mut TestAppContext) {
2303 let cx = cx.add_empty_window();
2304
2305 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2307
2308 struct TestView(ListState);
2309 impl Render for TestView {
2310 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2311 list(self.0.clone(), |_, _, _| {
2312 div().h(px(50.)).w_full().into_any()
2313 })
2314 .w_full()
2315 .h_full()
2316 }
2317 }
2318
2319 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2320
2321 state.set_follow_mode(FollowMode::Tail);
2322
2323 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2324 view.clone().into_any_element()
2325 });
2326 assert!(state.is_following_tail());
2327
2328 cx.simulate_event(ScrollWheelEvent {
2330 position: point(px(50.), px(100.)),
2331 delta: ScrollDelta::Pixels(point(px(0.), px(50.))),
2332 ..Default::default()
2333 });
2334 assert!(!state.is_following_tail());
2335
2336 cx.simulate_event(ScrollWheelEvent {
2338 position: point(px(50.), px(100.)),
2339 delta: ScrollDelta::Pixels(point(px(0.), px(-10000.))),
2340 ..Default::default()
2341 });
2342
2343 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2346 view.clone().into_any_element()
2347 });
2348 assert!(
2349 state.is_following_tail(),
2350 "follow_tail should re-engage after scrolling back to the bottom"
2351 );
2352 }
2353
2354 #[open_gpui::test]
2357 fn test_follow_tail_reengagement_not_fooled_by_unmeasured_items(cx: &mut TestAppContext) {
2358 let cx = cx.add_empty_window();
2359
2360 let state = ListState::new(20, crate::ListAlignment::Top, px(1000.));
2364
2365 struct TestView(ListState);
2366 impl Render for TestView {
2367 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2368 list(self.0.clone(), |_, _, _| {
2369 div().h(px(50.)).w_full().into_any()
2370 })
2371 .w_full()
2372 .h_full()
2373 }
2374 }
2375
2376 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2377
2378 state.set_follow_mode(FollowMode::Tail);
2379
2380 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2381 view.clone().into_any_element()
2382 });
2383 assert!(state.is_following_tail());
2384
2385 cx.simulate_event(ScrollWheelEvent {
2389 position: point(px(50.), px(100.)),
2390 delta: ScrollDelta::Pixels(point(px(0.), px(200.))),
2391 ..Default::default()
2392 });
2393 assert!(!state.is_following_tail());
2394
2395 state.remeasure_items(19..20);
2399
2400 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2406 view.clone().into_any_element()
2407 });
2408 assert!(
2409 !state.is_following_tail(),
2410 "follow_tail should not falsely re-engage due to an unmeasured item \
2411 reducing items.summary().height"
2412 );
2413 }
2414
2415 #[open_gpui::test]
2416 fn test_follow_tail_reengages_after_scrollbar_disengagement(cx: &mut TestAppContext) {
2417 let cx = cx.add_empty_window();
2418
2419 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
2421
2422 struct TestView(ListState);
2423 impl Render for TestView {
2424 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2425 list(self.0.clone(), |_, _, _| {
2426 div().h(px(50.)).w_full().into_any()
2427 })
2428 .w_full()
2429 .h_full()
2430 }
2431 }
2432
2433 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2434
2435 state.set_follow_mode(FollowMode::Tail);
2436 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2437 view.clone().into_any_element()
2438 });
2439 assert!(state.is_following_tail());
2440
2441 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
2443 assert!(!state.is_following_tail());
2444
2445 state.set_offset_from_scrollbar(point(px(0.), px(-300.)));
2448 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2449 view.into_any_element()
2450 });
2451 assert!(
2452 state.is_following_tail(),
2453 "follow_tail should re-engage after scrolling back to the bottom via the scrollbar"
2454 );
2455 }
2456
2457 #[open_gpui::test]
2458 fn test_follow_tail_reengages_after_scrollbar_drag_to_bottom_while_growing(
2459 cx: &mut TestAppContext,
2460 ) {
2461 let cx = cx.add_empty_window();
2462
2463 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
2464
2465 struct TestView(ListState);
2466 impl Render for TestView {
2467 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2468 list(self.0.clone(), |_, _, _| {
2469 div().h(px(50.)).w_full().into_any()
2470 })
2471 .w_full()
2472 .h_full()
2473 }
2474 }
2475
2476 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2477
2478 state.set_follow_mode(FollowMode::Tail);
2479 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2480 view.clone().into_any_element()
2481 });
2482 assert!(state.is_following_tail());
2483
2484 state.scrollbar_drag_started();
2485
2486 state.splice(10..10, 10);
2487 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2488 view.clone().into_any_element()
2489 });
2490
2491 state.set_offset_from_scrollbar(point(px(0.), px(-300.)));
2492 state.scrollbar_drag_ended();
2493
2494 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2495 view.into_any_element()
2496 });
2497
2498 assert!(
2499 state.is_following_tail(),
2500 "follow_tail should re-engage when the user drags the scrollbar to \
2501 the bottom of its track, even when content has grown during the drag \
2502 (so frozen_bottom < live_bottom)"
2503 );
2504 }
2505}