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.pending_scroll = None;
351 state.scrollbar_drag_start_height = None;
352 state.items.summary().count
353 };
354
355 self.splice(0..old_count, element_count);
356 }
357
358 pub fn remeasure(&self) {
363 let count = self.item_count();
364 self.remeasure_items_with_scroll_anchor(0..count, ScrollAnchor::Proportional);
365 }
366
367 pub fn remeasure_items(&self, range: Range<usize>) {
375 self.remeasure_items_with_scroll_anchor(range, ScrollAnchor::Absolute);
376 }
377
378 fn remeasure_items_with_scroll_anchor(&self, range: Range<usize>, scroll_anchor: ScrollAnchor) {
379 let state = &mut *self.0.borrow_mut();
380
381 if let Some(scroll_top) = state.logical_scroll_top {
382 if range.contains(&scroll_top.item_ix) {
383 state.pending_scroll = match scroll_anchor {
384 ScrollAnchor::Absolute => Some(PendingScroll::Absolute {
385 item_ix: scroll_top.item_ix,
386 offset: scroll_top.offset_in_item,
387 }),
388 ScrollAnchor::Proportional => {
389 let mut cursor = state.items.cursor::<Count>(());
394 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
395
396 cursor
397 .item()
398 .and_then(|item| {
399 item.size().map(|size| {
400 let fraction = if size.height.0 > 0.0 {
401 (scroll_top.offset_in_item.0 / size.height.0)
402 .clamp(0.0, 1.0)
403 } else {
404 0.0
405 };
406
407 PendingScroll::Proportional(PendingScrollFraction {
408 item_ix: scroll_top.item_ix,
409 fraction,
410 })
411 })
412 })
413 .or_else(|| state.pending_scroll.clone())
414 }
415 };
416 }
417 }
418
419 let new_items = {
422 let mut cursor = state.items.cursor::<Count>(());
423 let mut new_items = cursor.slice(&Count(range.start), Bias::Right);
424 let invalidated = cursor.slice(&Count(range.end), Bias::Right);
425 new_items.extend(
426 invalidated.iter().map(|item| ListItem::Unmeasured {
427 size_hint: item.size_hint(),
428 focus_handle: item.focus_handle(),
429 }),
430 (),
431 );
432 new_items.append(cursor.suffix(), ());
433 new_items
434 };
435 state.items = new_items;
436 state.measuring_behavior.reset();
437 }
438
439 pub fn item_count(&self) -> usize {
441 self.0.borrow().items.summary().count
442 }
443
444 pub fn is_scrolled_to_end(&self) -> Option<bool> {
447 let state = self.0.borrow();
448 let bounds = state.last_layout_bounds?;
449 let summary = state.items.summary();
450 if summary.has_unknown_height {
451 return None;
452 }
453 let padding = state.last_padding.unwrap_or_default();
454 let content_height = summary.height + padding.top + padding.bottom;
455 let scroll_max = (content_height - bounds.size.height).max(px(0.));
456 if scroll_max <= px(0.) {
457 return None;
458 }
459 let scroll_top = state.scroll_top(&state.logical_scroll_top());
460 Some(scroll_top >= scroll_max)
461 }
462
463 pub fn splice(&self, old_range: Range<usize>, count: usize) {
466 self.splice_focusable(old_range, (0..count).map(|_| None))
467 }
468
469 pub fn splice_focusable(
474 &self,
475 old_range: Range<usize>,
476 focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
477 ) {
478 let state = &mut *self.0.borrow_mut();
479
480 let mut old_items = state.items.cursor::<Count>(());
481 let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right);
482 old_items.seek_forward(&Count(old_range.end), Bias::Right);
483
484 let mut spliced_count = 0;
485 new_items.extend(
486 focus_handles.into_iter().map(|focus_handle| {
487 spliced_count += 1;
488 ListItem::Unmeasured {
489 size_hint: None,
490 focus_handle,
491 }
492 }),
493 (),
494 );
495 new_items.append(old_items.suffix(), ());
496 drop(old_items);
497 state.items = new_items;
498
499 if let Some(ListOffset {
500 item_ix,
501 offset_in_item,
502 }) = state.logical_scroll_top.as_mut()
503 {
504 if old_range.contains(item_ix) {
505 *item_ix = old_range.start;
506 *offset_in_item = px(0.);
507 } else if old_range.end <= *item_ix {
508 *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count;
509 }
510 }
511 }
512
513 pub fn set_scroll_handler(
515 &self,
516 handler: impl FnMut(&ListScrollEvent, &mut Window, &mut App) + 'static,
517 ) {
518 self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
519 }
520
521 pub fn logical_scroll_top(&self) -> ListOffset {
523 self.0.borrow().logical_scroll_top()
524 }
525
526 pub fn scroll_by(&self, distance: Pixels) {
528 if distance == px(0.) {
529 return;
530 }
531
532 let current_offset = self.logical_scroll_top();
533 let state = &mut *self.0.borrow_mut();
534
535 if distance < px(0.) {
536 state.follow_state.stop_following();
537 }
538
539 let mut cursor = state.items.cursor::<ListItemSummary>(());
540 cursor.seek(&Count(current_offset.item_ix), Bias::Right);
541
542 let start_pixel_offset = cursor.start().height + current_offset.offset_in_item;
543 let new_pixel_offset = (start_pixel_offset + distance).max(px(0.));
544 if new_pixel_offset > start_pixel_offset {
545 cursor.seek_forward(&Height(new_pixel_offset), Bias::Right);
546 } else {
547 cursor.seek(&Height(new_pixel_offset), Bias::Right);
548 }
549
550 let scroll_top = ListOffset {
551 item_ix: cursor.start().count,
552 offset_in_item: new_pixel_offset - cursor.start().height,
553 };
554 drop(cursor);
555 state.rebase_pending_scroll(scroll_top);
556 state.logical_scroll_top = Some(scroll_top);
557 }
558
559 pub fn scroll_to_end(&self) {
566 let state = &mut *self.0.borrow_mut();
567 let item_count = state.items.summary().count;
568 state.pending_scroll = None;
569 state.logical_scroll_top = Some(ListOffset {
570 item_ix: item_count,
571 offset_in_item: px(0.),
572 });
573 }
574
575 pub fn set_follow_mode(&self, mode: FollowMode) {
580 let state = &mut *self.0.borrow_mut();
581
582 match mode {
583 FollowMode::Normal => {
584 state.follow_state = FollowState::Normal;
585 }
586 FollowMode::Tail => {
587 state.follow_state = FollowState::Tail { is_following: true };
588 if matches!(mode, FollowMode::Tail) {
589 let item_count = state.items.summary().count;
590 state.logical_scroll_top = Some(ListOffset {
591 item_ix: item_count,
592 offset_in_item: px(0.),
593 });
594 }
595 }
596 }
597 }
598
599 pub fn is_following_tail(&self) -> bool {
602 matches!(
603 self.0.borrow().follow_state,
604 FollowState::Tail { is_following: true }
605 )
606 }
607
608 pub fn scroll_to(&self, mut scroll_top: ListOffset) {
610 let state = &mut *self.0.borrow_mut();
611 let item_count = state.items.summary().count;
612 if scroll_top.item_ix >= item_count {
613 scroll_top.item_ix = item_count;
614 scroll_top.offset_in_item = px(0.);
615 }
616
617 if scroll_top.item_ix < item_count {
618 state.follow_state.stop_following();
619 }
620
621 state.rebase_pending_scroll(scroll_top);
622 state.logical_scroll_top = Some(scroll_top);
623 }
624
625 pub fn scroll_to_reveal_item(&self, ix: usize) {
627 let state = &mut *self.0.borrow_mut();
628
629 let mut scroll_top = state.logical_scroll_top();
630 let height = state
631 .last_layout_bounds
632 .map_or(px(0.), |bounds| bounds.size.height);
633 let padding = state.last_padding.unwrap_or_default();
634
635 if ix <= scroll_top.item_ix {
636 scroll_top.item_ix = ix;
637 scroll_top.offset_in_item = px(0.);
638 } else {
639 let mut cursor = state.items.cursor::<ListItemSummary>(());
640 cursor.seek(&Count(ix + 1), Bias::Right);
641 let bottom = cursor.start().height + padding.top;
642 let goal_top = px(0.).max(bottom - height + padding.bottom);
643
644 cursor.seek(&Height(goal_top), Bias::Left);
645 let start_ix = cursor.start().count;
646 let start_item_top = cursor.start().height;
647
648 if start_ix >= scroll_top.item_ix {
649 scroll_top.item_ix = start_ix;
650 scroll_top.offset_in_item = goal_top - start_item_top;
651 }
652 }
653
654 state.rebase_pending_scroll(scroll_top);
655 state.logical_scroll_top = Some(scroll_top);
656 }
657
658 pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
661 let state = &*self.0.borrow();
662
663 let bounds = state.last_layout_bounds.unwrap_or_default();
664 let scroll_top = state.logical_scroll_top();
665 if ix < scroll_top.item_ix {
666 return None;
667 }
668
669 let mut cursor = state.items.cursor::<Dimensions<Count, Height>>(());
670 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
671
672 let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item;
673
674 cursor.seek_forward(&Count(ix), Bias::Right);
675 if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
676 let &Dimensions(Count(count), Height(top), _) = cursor.start();
677 if count == ix {
678 let top = bounds.top() + top - scroll_top;
679 return Some(Bounds::from_corners(
680 point(bounds.left(), top),
681 point(bounds.right(), top + size.height),
682 ));
683 }
684 }
685 None
686 }
687
688 pub fn scrollbar_drag_started(&self) {
693 let mut state = self.0.borrow_mut();
694 state.scrollbar_drag_start_height = Some(state.items.summary().height);
695 }
696
697 pub fn scrollbar_drag_ended(&self) {
701 self.0.borrow_mut().scrollbar_drag_start_height.take();
702 }
703
704 pub fn is_scrollbar_dragging(&self) -> bool {
711 self.0.borrow().scrollbar_drag_start_height.is_some()
712 }
713
714 pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
716 self.0.borrow_mut().set_offset_from_scrollbar(point);
717 }
718
719 pub fn max_offset_for_scrollbar(&self) -> Point<Pixels> {
722 let state = self.0.borrow();
723 point(Pixels::ZERO, state.max_scroll_offset())
724 }
725
726 pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
731 let state = &self.0.borrow();
732
733 if state.logical_scroll_top.is_none() && state.alignment == ListAlignment::Bottom {
734 return Point::new(px(0.), -state.max_scroll_offset());
735 }
736
737 let logical_scroll_top = state.logical_scroll_top();
738
739 let mut cursor = state.items.cursor::<ListItemSummary>(());
740 let summary: ListItemSummary =
741 cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right);
742 let offset = summary.height + logical_scroll_top.offset_in_item;
743
744 Point::new(px(0.), -offset)
745 }
746
747 pub fn viewport_bounds(&self) -> Bounds<Pixels> {
749 self.0.borrow().last_layout_bounds.unwrap_or_default()
750 }
751
752 pub fn item_is_above_viewport(&self, ix: usize) -> Option<bool> {
760 let viewport_bounds = self.0.borrow().last_layout_bounds?;
761
762 let scroll_top = self.logical_scroll_top();
763 if ix < scroll_top.item_ix {
764 return Some(true);
767 }
768
769 let item_bounds = self.bounds_for_item(ix)?;
770 Some(item_bounds.bottom() <= viewport_bounds.top())
771 }
772
773 pub fn item_is_below_viewport(&self, ix: usize) -> Option<bool> {
779 let viewport_bounds = self.0.borrow().last_layout_bounds?;
780
781 let scroll_top = self.logical_scroll_top();
782 if ix < scroll_top.item_ix {
783 return Some(false);
786 }
787
788 let item_bounds = self.bounds_for_item(ix)?;
789 Some(item_bounds.top() >= viewport_bounds.bottom())
790 }
791}
792
793impl StateInner {
794 fn rebase_pending_scroll(&mut self, scroll_top: ListOffset) {
795 let Some(pending) = self.pending_scroll.take() else {
796 return;
797 };
798
799 if scroll_top.item_ix >= self.items.summary().count {
800 return;
801 }
802
803 self.pending_scroll = match pending {
804 PendingScroll::Absolute { .. } => Some(PendingScroll::Absolute {
805 item_ix: scroll_top.item_ix,
806 offset: scroll_top.offset_in_item,
807 }),
808 PendingScroll::Proportional(_) => {
809 let mut cursor = self.items.cursor::<Count>(());
810 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
811 cursor
812 .item()
813 .and_then(|item| item.size_hint())
814 .filter(|size| size.height.0 > 0.0)
815 .map(|size| {
816 PendingScroll::Proportional(PendingScrollFraction {
817 item_ix: scroll_top.item_ix,
818 fraction: (scroll_top.offset_in_item.0 / size.height.0).clamp(0.0, 1.0),
819 })
820 })
821 }
822 };
823 }
824
825 fn max_scroll_offset(&self) -> Pixels {
826 let bounds = self.last_layout_bounds.unwrap_or_default();
827 let height = self
828 .scrollbar_drag_start_height
829 .unwrap_or_else(|| self.items.summary().height);
830 (height - bounds.size.height).max(px(0.))
831 }
832
833 fn visible_range(
834 items: &SumTree<ListItem>,
835 height: Pixels,
836 scroll_top: &ListOffset,
837 ) -> Range<usize> {
838 let mut cursor = items.cursor::<ListItemSummary>(());
839 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
840 let start_y = cursor.start().height + scroll_top.offset_in_item;
841 cursor.seek_forward(&Height(start_y + height), Bias::Left);
842 scroll_top.item_ix..cursor.start().count + 1
843 }
844
845 fn scroll(
846 &mut self,
847 scroll_top: &ListOffset,
848 height: Pixels,
849 delta: Point<Pixels>,
850 current_view: EntityId,
851 window: &mut Window,
852 cx: &mut App,
853 ) {
854 if self.reset {
857 return;
858 }
859
860 let padding = self.last_padding.unwrap_or_default();
861 let scroll_max =
862 (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
863 let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
864 .max(px(0.))
865 .min(scroll_max);
866
867 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
868 self.pending_scroll = None;
869 self.logical_scroll_top = None;
870 } else {
871 let (start, ..) =
872 self.items
873 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
874 let scroll_top = ListOffset {
875 item_ix: start.count,
876 offset_in_item: new_scroll_top - start.height,
877 };
878 self.rebase_pending_scroll(scroll_top);
879 self.logical_scroll_top = Some(scroll_top);
880 }
881
882 if delta.y > px(0.) {
883 self.follow_state.stop_following();
884 }
885
886 if let Some(handler) = self.scroll_handler.as_mut() {
887 let visible_range = Self::visible_range(&self.items, height, scroll_top);
888 handler(
889 &ListScrollEvent {
890 visible_range,
891 count: self.items.summary().count,
892 is_scrolled: self.logical_scroll_top.is_some(),
893 is_following_tail: matches!(
894 self.follow_state,
895 FollowState::Tail { is_following: true }
896 ),
897 },
898 window,
899 cx,
900 );
901 }
902
903 cx.notify(current_view);
904 }
905
906 fn logical_scroll_top(&self) -> ListOffset {
907 self.logical_scroll_top
908 .unwrap_or_else(|| match self.alignment {
909 ListAlignment::Top => ListOffset {
910 item_ix: 0,
911 offset_in_item: px(0.),
912 },
913 ListAlignment::Bottom => ListOffset {
914 item_ix: self.items.summary().count,
915 offset_in_item: px(0.),
916 },
917 })
918 }
919
920 fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
921 let (start, ..) = self.items.find::<ListItemSummary, _>(
922 (),
923 &Count(logical_scroll_top.item_ix),
924 Bias::Right,
925 );
926 start.height + logical_scroll_top.offset_in_item
927 }
928
929 fn layout_all_items(
930 &mut self,
931 available_width: Pixels,
932 render_item: &mut RenderItemFn,
933 window: &mut Window,
934 cx: &mut App,
935 ) {
936 match &mut self.measuring_behavior {
937 ListMeasuringBehavior::Visible => {
938 return;
939 }
940 ListMeasuringBehavior::Measure(has_measured) => {
941 if *has_measured {
942 return;
943 }
944 *has_measured = true;
945 }
946 }
947
948 let mut cursor = self.items.cursor::<Count>(());
949 let available_item_space = size(
950 AvailableSpace::Definite(available_width),
951 AvailableSpace::MinContent,
952 );
953
954 let mut measured_items = Vec::default();
955
956 for (ix, item) in cursor.enumerate() {
957 let size = item.size().unwrap_or_else(|| {
958 let mut element = render_item(ix, window, cx);
959 element.layout_as_root(available_item_space, window, cx)
960 });
961
962 measured_items.push(ListItem::Measured {
963 size,
964 focus_handle: item.focus_handle(),
965 });
966 }
967
968 self.items = SumTree::from_iter(measured_items, ());
969 }
970
971 fn layout_items(
972 &mut self,
973 available_width: Option<Pixels>,
974 available_height: Pixels,
975 padding: &Edges<Pixels>,
976 render_item: &mut RenderItemFn,
977 window: &mut Window,
978 cx: &mut App,
979 ) -> LayoutItemsResponse {
980 let old_items = self.items.clone();
981 let mut measured_items = VecDeque::new();
982 let mut item_layouts = VecDeque::new();
983 let mut rendered_height = padding.top;
984 let mut max_item_width = px(0.);
985 let mut scroll_top = self.logical_scroll_top();
986
987 if self.follow_state.is_following() {
988 scroll_top = ListOffset {
989 item_ix: self.items.summary().count,
990 offset_in_item: px(0.),
991 };
992 self.logical_scroll_top = Some(scroll_top);
993 }
994
995 let mut rendered_focused_item = false;
996
997 let available_item_space = size(
998 available_width.map_or(AvailableSpace::MinContent, |width| {
999 AvailableSpace::Definite(width)
1000 }),
1001 AvailableSpace::MinContent,
1002 );
1003
1004 let mut cursor = old_items.cursor::<Count>(());
1005
1006 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
1008 for (ix, item) in cursor.by_ref().enumerate() {
1009 let visible_height = rendered_height - scroll_top.offset_in_item;
1010 if visible_height >= available_height + self.overdraw {
1011 break;
1012 }
1013
1014 let mut size = item.size();
1016
1017 if visible_height < available_height || size.is_none() {
1019 let item_index = scroll_top.item_ix + ix;
1020 let mut element = render_item(item_index, window, cx);
1021 let element_size = element.layout_as_root(available_item_space, window, cx);
1022 size = Some(element_size);
1023
1024 if ix == 0 {
1027 if let Some(pending_scroll) = self.pending_scroll.take() {
1028 match pending_scroll {
1029 PendingScroll::Absolute { item_ix, offset }
1030 if item_ix == scroll_top.item_ix =>
1031 {
1032 scroll_top.offset_in_item = offset.min(element_size.height);
1033 self.logical_scroll_top = Some(scroll_top);
1034 }
1035 PendingScroll::Proportional(pending_scroll)
1036 if pending_scroll.item_ix == scroll_top.item_ix =>
1037 {
1038 scroll_top.offset_in_item =
1041 Pixels(pending_scroll.fraction * element_size.height.0);
1042 self.logical_scroll_top = Some(scroll_top);
1043 }
1044 _ => {}
1045 }
1046 }
1047 }
1048
1049 if visible_height < available_height {
1050 item_layouts.push_back(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 }
1059 }
1060
1061 let size = size.unwrap();
1062 rendered_height += size.height;
1063 max_item_width = max_item_width.max(size.width);
1064 measured_items.push_back(ListItem::Measured {
1065 size,
1066 focus_handle: item.focus_handle(),
1067 });
1068 }
1069 rendered_height += padding.bottom;
1070
1071 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
1073
1074 if rendered_height - scroll_top.offset_in_item < available_height {
1077 while rendered_height < available_height {
1078 cursor.prev();
1079 if let Some(item) = cursor.item() {
1080 let item_index = cursor.start().0;
1081 let mut element = render_item(item_index, window, cx);
1082 let element_size = element.layout_as_root(available_item_space, window, cx);
1083 let focus_handle = item.focus_handle();
1084 rendered_height += element_size.height;
1085 measured_items.push_front(ListItem::Measured {
1086 size: element_size,
1087 focus_handle,
1088 });
1089 item_layouts.push_front(ItemLayout {
1090 index: item_index,
1091 element,
1092 size: element_size,
1093 });
1094 if item.contains_focused(window, cx) {
1095 rendered_focused_item = true;
1096 }
1097 } else {
1098 break;
1099 }
1100 }
1101
1102 scroll_top = ListOffset {
1103 item_ix: cursor.start().0,
1104 offset_in_item: rendered_height - available_height,
1105 };
1106
1107 match self.alignment {
1108 ListAlignment::Top => {
1109 scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
1110 self.logical_scroll_top = Some(scroll_top);
1111 }
1112 ListAlignment::Bottom => {
1113 scroll_top = ListOffset {
1114 item_ix: cursor.start().0,
1115 offset_in_item: rendered_height - available_height,
1116 };
1117 self.logical_scroll_top = None;
1118 }
1119 };
1120 }
1121
1122 let mut leading_overdraw = scroll_top.offset_in_item;
1124 while leading_overdraw < self.overdraw {
1125 cursor.prev();
1126 if let Some(item) = cursor.item() {
1127 let size = if let ListItem::Measured { size, .. } = item {
1128 *size
1129 } else {
1130 let mut element = render_item(cursor.start().0, window, cx);
1131 element.layout_as_root(available_item_space, window, cx)
1132 };
1133
1134 leading_overdraw += size.height;
1135 measured_items.push_front(ListItem::Measured {
1136 size,
1137 focus_handle: item.focus_handle(),
1138 });
1139 } else {
1140 break;
1141 }
1142 }
1143
1144 let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
1145 let mut cursor = old_items.cursor::<Count>(());
1146 let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right);
1147 new_items.extend(measured_items, ());
1148 cursor.seek(&Count(measured_range.end), Bias::Right);
1149 new_items.append(cursor.suffix(), ());
1150 self.items = new_items;
1151
1152 if self.follow_state.has_stopped_following() {
1156 let padding = self.last_padding.unwrap_or_default();
1157 let total_height = self.items.summary().height + padding.top + padding.bottom;
1158 let scroll_offset = self.scroll_top(&scroll_top);
1159 if scroll_offset + available_height >= total_height - px(1.0) {
1160 self.follow_state.start_following();
1161 }
1162 }
1163
1164 if !rendered_focused_item {
1168 let mut cursor = self
1169 .items
1170 .filter::<_, Count>((), |summary| summary.has_focus_handles);
1171 cursor.next();
1172 while let Some(item) = cursor.item() {
1173 if item.contains_focused(window, cx) {
1174 let item_index = cursor.start().0;
1175 let mut element = render_item(cursor.start().0, window, cx);
1176 let size = element.layout_as_root(available_item_space, window, cx);
1177 item_layouts.push_back(ItemLayout {
1178 index: item_index,
1179 element,
1180 size,
1181 });
1182 break;
1183 }
1184 cursor.next();
1185 }
1186 }
1187
1188 LayoutItemsResponse {
1189 max_item_width,
1190 scroll_top,
1191 item_layouts,
1192 }
1193 }
1194
1195 fn prepaint_items(
1196 &mut self,
1197 bounds: Bounds<Pixels>,
1198 padding: Edges<Pixels>,
1199 autoscroll: bool,
1200 render_item: &mut RenderItemFn,
1201 window: &mut Window,
1202 cx: &mut App,
1203 ) -> Result<LayoutItemsResponse, ListOffset> {
1204 window.transact(|window| {
1205 match self.measuring_behavior {
1206 ListMeasuringBehavior::Measure(has_measured) if !has_measured => {
1207 self.layout_all_items(bounds.size.width, render_item, window, cx);
1208 }
1209 _ => {}
1210 }
1211
1212 let mut layout_response = self.layout_items(
1213 Some(bounds.size.width),
1214 bounds.size.height,
1215 &padding,
1216 render_item,
1217 window,
1218 cx,
1219 );
1220
1221 window.take_autoscroll();
1223
1224 if bounds.size.height > padding.top + padding.bottom {
1226 let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
1227 item_origin.y -= layout_response.scroll_top.offset_in_item;
1228 for item in &mut layout_response.item_layouts {
1229 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1230 item.element.prepaint_at(item_origin, window, cx);
1231 });
1232
1233 if let Some(autoscroll_bounds) = window.take_autoscroll()
1234 && autoscroll
1235 {
1236 if autoscroll_bounds.top() < bounds.top() {
1237 return Err(ListOffset {
1238 item_ix: item.index,
1239 offset_in_item: autoscroll_bounds.top() - item_origin.y,
1240 });
1241 } else if autoscroll_bounds.bottom() > bounds.bottom() {
1242 let mut cursor = self.items.cursor::<Count>(());
1243 cursor.seek(&Count(item.index), Bias::Right);
1244 let mut height = bounds.size.height - padding.top - padding.bottom;
1245
1246 height -= autoscroll_bounds.bottom() - item_origin.y;
1248
1249 while height > Pixels::ZERO {
1251 cursor.prev();
1252 let Some(item) = cursor.item() else { break };
1253
1254 let size = item.size().unwrap_or_else(|| {
1255 let mut item = render_item(cursor.start().0, window, cx);
1256 let item_available_size =
1257 size(bounds.size.width.into(), AvailableSpace::MinContent);
1258 item.layout_as_root(item_available_size, window, cx)
1259 });
1260 height -= size.height;
1261 }
1262
1263 return Err(ListOffset {
1264 item_ix: cursor.start().0,
1265 offset_in_item: if height < Pixels::ZERO {
1266 -height
1267 } else {
1268 Pixels::ZERO
1269 },
1270 });
1271 }
1272 }
1273
1274 item_origin.y += item.size.height;
1275 }
1276 } else {
1277 layout_response.item_layouts.clear();
1278 }
1279
1280 Ok(layout_response)
1281 })
1282 }
1283
1284 fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
1287 let Some(bounds) = self.last_layout_bounds else {
1288 return;
1289 };
1290 let height = bounds.size.height;
1291
1292 let padding = self.last_padding.unwrap_or_default();
1293 let content_height = self
1296 .scrollbar_drag_start_height
1297 .unwrap_or_else(|| self.items.summary().height);
1298 let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
1299 let new_scroll_top = (-point.y).max(px(0.)).min(scroll_max);
1300
1301 let dragged_to_end =
1304 scroll_max > px(0.) && new_scroll_top >= (scroll_max - px(1.0)).max(px(0.));
1305 if dragged_to_end && matches!(self.follow_state, FollowState::Tail { .. }) {
1306 self.follow_state = FollowState::Tail { is_following: true };
1307 let item_count = self.items.summary().count;
1308 self.pending_scroll = None;
1309 self.logical_scroll_top = Some(ListOffset {
1310 item_ix: item_count,
1311 offset_in_item: px(0.),
1312 });
1313 return;
1314 }
1315
1316 self.follow_state.stop_following();
1317
1318 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
1319 self.pending_scroll = None;
1320 self.logical_scroll_top = None;
1321 } else {
1322 let (start, _, _) =
1323 self.items
1324 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
1325
1326 let scroll_top = ListOffset {
1327 item_ix: start.count,
1328 offset_in_item: new_scroll_top - start.height,
1329 };
1330 self.rebase_pending_scroll(scroll_top);
1331 self.logical_scroll_top = Some(scroll_top);
1332 }
1333 }
1334}
1335
1336impl std::fmt::Debug for ListItem {
1337 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1338 match self {
1339 Self::Unmeasured { .. } => write!(f, "Unrendered"),
1340 Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
1341 }
1342 }
1343}
1344
1345#[derive(Debug, Clone, Copy, Default)]
1348pub struct ListOffset {
1349 pub item_ix: usize,
1351 pub offset_in_item: Pixels,
1353}
1354
1355impl Element for List {
1356 type RequestLayoutState = ();
1357 type PrepaintState = ListPrepaintState;
1358
1359 fn id(&self) -> Option<crate::ElementId> {
1360 None
1361 }
1362
1363 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1364 None
1365 }
1366
1367 fn request_layout(
1368 &mut self,
1369 _id: Option<&GlobalElementId>,
1370 _inspector_id: Option<&InspectorElementId>,
1371 window: &mut Window,
1372 cx: &mut App,
1373 ) -> (crate::LayoutId, Self::RequestLayoutState) {
1374 let layout_id = match self.sizing_behavior {
1375 ListSizingBehavior::Infer => {
1376 let mut style = Style::default();
1377 style.overflow.y = Overflow::Scroll;
1378 style.refine(&self.style);
1379 window.with_text_style(style.text_style().cloned(), |window| {
1380 let state = &mut *self.state.0.borrow_mut();
1381
1382 let available_height = if let Some(last_bounds) = state.last_layout_bounds {
1383 last_bounds.size.height
1384 } else {
1385 state.overdraw
1388 };
1389 let padding = style.padding.to_pixels(
1390 state.last_layout_bounds.unwrap_or_default().size.into(),
1391 window.rem_size(),
1392 );
1393
1394 let layout_response = state.layout_items(
1395 None,
1396 available_height,
1397 &padding,
1398 &mut self.render_item,
1399 window,
1400 cx,
1401 );
1402 let max_element_width = layout_response.max_item_width;
1403
1404 let summary = state.items.summary();
1405 let total_height = summary.height;
1406
1407 window.request_measured_layout(
1408 style,
1409 move |known_dimensions, available_space, _window, _cx| {
1410 let width =
1411 known_dimensions
1412 .width
1413 .unwrap_or(match available_space.width {
1414 AvailableSpace::Definite(x) => x,
1415 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1416 max_element_width
1417 }
1418 });
1419 let height = match available_space.height {
1420 AvailableSpace::Definite(height) => total_height.min(height),
1421 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1422 total_height
1423 }
1424 };
1425 size(width, height)
1426 },
1427 )
1428 })
1429 }
1430 ListSizingBehavior::Auto => {
1431 let mut style = Style::default();
1432 style.refine(&self.style);
1433 window.with_text_style(style.text_style().cloned(), |window| {
1434 window.request_layout(style, None, cx)
1435 })
1436 }
1437 };
1438 (layout_id, ())
1439 }
1440
1441 fn prepaint(
1442 &mut self,
1443 _id: Option<&GlobalElementId>,
1444 _inspector_id: Option<&InspectorElementId>,
1445 bounds: Bounds<Pixels>,
1446 _: &mut Self::RequestLayoutState,
1447 window: &mut Window,
1448 cx: &mut App,
1449 ) -> ListPrepaintState {
1450 let state = &mut *self.state.0.borrow_mut();
1451 state.reset = false;
1452
1453 let mut style = Style::default();
1454 style.refine(&self.style);
1455
1456 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1457
1458 if state
1460 .last_layout_bounds
1461 .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width)
1462 {
1463 let new_items = SumTree::from_iter(
1464 state.items.iter().map(|item| ListItem::Unmeasured {
1465 size_hint: None,
1466 focus_handle: item.focus_handle(),
1467 }),
1468 (),
1469 );
1470
1471 state.items = new_items;
1472 state.measuring_behavior.reset();
1473 }
1474
1475 let padding = style
1476 .padding
1477 .to_pixels(bounds.size.into(), window.rem_size());
1478 let layout =
1479 match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) {
1480 Ok(layout) => layout,
1481 Err(autoscroll_request) => {
1482 state.logical_scroll_top = Some(autoscroll_request);
1483 state
1484 .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx)
1485 .unwrap()
1486 }
1487 };
1488
1489 state.last_layout_bounds = Some(bounds);
1490 state.last_padding = Some(padding);
1491 ListPrepaintState { hitbox, layout }
1492 }
1493
1494 fn paint(
1495 &mut self,
1496 _id: Option<&GlobalElementId>,
1497 _inspector_id: Option<&InspectorElementId>,
1498 bounds: Bounds<crate::Pixels>,
1499 _: &mut Self::RequestLayoutState,
1500 prepaint: &mut Self::PrepaintState,
1501 window: &mut Window,
1502 cx: &mut App,
1503 ) {
1504 let current_view = window.current_view();
1505 let list_state = self.state.clone();
1506 let height = bounds.size.height;
1507 let scroll_top = prepaint.layout.scroll_top;
1508 let hitbox_id = prepaint.hitbox.id;
1509 let mut accumulated_scroll_delta = ScrollDelta::default();
1510 window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
1511 if phase == DispatchPhase::Bubble
1512 && !window.default_prevented()
1513 && hitbox_id.should_handle_scroll(window)
1514 {
1515 accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
1516 let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
1517 list_state.0.borrow_mut().scroll(
1518 &scroll_top,
1519 height,
1520 pixel_delta,
1521 current_view,
1522 window,
1523 cx,
1524 )
1525 }
1526 });
1527
1528 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1529 for item in &mut prepaint.layout.item_layouts {
1530 item.element.paint(window, cx);
1531 }
1532 });
1533 }
1534}
1535
1536impl IntoElement for List {
1537 type Element = Self;
1538
1539 fn into_element(self) -> Self::Element {
1540 self
1541 }
1542}
1543
1544impl Styled for List {
1545 fn style(&mut self) -> &mut StyleRefinement {
1546 &mut self.style
1547 }
1548}
1549
1550impl open_gpui_sum_tree::Item for ListItem {
1551 type Summary = ListItemSummary;
1552
1553 fn summary(&self, _: ()) -> Self::Summary {
1554 match self {
1555 ListItem::Unmeasured {
1556 size_hint,
1557 focus_handle,
1558 } => ListItemSummary {
1559 count: 1,
1560 rendered_count: 0,
1561 unrendered_count: 1,
1562 height: if let Some(size) = size_hint {
1563 size.height
1564 } else {
1565 px(0.)
1566 },
1567 has_focus_handles: focus_handle.is_some(),
1568 has_unknown_height: size_hint.is_none(),
1569 },
1570 ListItem::Measured {
1571 size, focus_handle, ..
1572 } => ListItemSummary {
1573 count: 1,
1574 rendered_count: 1,
1575 unrendered_count: 0,
1576 height: size.height,
1577 has_focus_handles: focus_handle.is_some(),
1578 has_unknown_height: false,
1579 },
1580 }
1581 }
1582}
1583
1584impl open_gpui_sum_tree::ContextLessSummary for ListItemSummary {
1585 fn zero() -> Self {
1586 Default::default()
1587 }
1588
1589 fn add_summary(&mut self, summary: &Self) {
1590 self.count += summary.count;
1591 self.rendered_count += summary.rendered_count;
1592 self.unrendered_count += summary.unrendered_count;
1593 self.height += summary.height;
1594 self.has_focus_handles |= summary.has_focus_handles;
1595 self.has_unknown_height |= summary.has_unknown_height;
1596 }
1597}
1598
1599impl<'a> open_gpui_sum_tree::Dimension<'a, ListItemSummary> for Count {
1600 fn zero(_cx: ()) -> Self {
1601 Default::default()
1602 }
1603
1604 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1605 self.0 += summary.count;
1606 }
1607}
1608
1609impl<'a> open_gpui_sum_tree::Dimension<'a, ListItemSummary> for Height {
1610 fn zero(_cx: ()) -> Self {
1611 Default::default()
1612 }
1613
1614 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1615 self.0 += summary.height;
1616 }
1617}
1618
1619impl open_gpui_sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Count {
1620 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1621 self.0.partial_cmp(&other.count).unwrap()
1622 }
1623}
1624
1625impl open_gpui_sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height {
1626 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1627 self.0.partial_cmp(&other.height).unwrap()
1628 }
1629}
1630
1631#[cfg(test)]
1632mod test {
1633
1634 use open_gpui::{ScrollDelta, ScrollWheelEvent};
1635 use std::cell::Cell;
1636 use std::rc::Rc;
1637
1638 use crate::{
1639 AppContext, Context, Element, FollowMode, InteractiveElement, IntoElement, ListState,
1640 ParentElement, Render, Styled, TestAppContext, Window, div, list, point, px, size,
1641 util::FluentBuilder,
1642 };
1643
1644 #[open_gpui::test]
1645 fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
1646 let cx = cx.add_empty_window();
1647
1648 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1649
1650 state.scroll_to(open_gpui::ListOffset {
1652 item_ix: 0,
1653 offset_in_item: px(0.0),
1654 });
1655
1656 struct TestView(ListState);
1657 impl Render for TestView {
1658 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1659 list(self.0.clone(), |_, _, _| {
1660 div().h(px(10.)).w_full().into_any()
1661 })
1662 .w_full()
1663 .h_full()
1664 }
1665 }
1666
1667 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1669 cx.new(|_| TestView(state.clone())).into_any_element()
1670 });
1671
1672 state.reset(5);
1674
1675 cx.simulate_event(ScrollWheelEvent {
1677 position: point(px(1.), px(1.)),
1678 delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
1679 ..Default::default()
1680 });
1681
1682 assert_eq!(state.logical_scroll_top().item_ix, 0);
1684 assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
1685 }
1686
1687 #[open_gpui::test]
1688 fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) {
1689 let cx = cx.add_empty_window();
1690
1691 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1692
1693 struct TestView(ListState);
1694 impl Render for TestView {
1695 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1696 list(self.0.clone(), |_, _, _| {
1697 div().h(px(20.)).w_full().into_any()
1698 })
1699 .w_full()
1700 .h_full()
1701 }
1702 }
1703
1704 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
1706 cx.new(|_| TestView(state.clone())).into_any_element()
1707 });
1708
1709 state.scroll_by(px(30.));
1711
1712 let offset = state.logical_scroll_top();
1714 assert_eq!(offset.item_ix, 1);
1715 assert_eq!(offset.offset_in_item, px(10.));
1716
1717 state.scroll_by(px(-30.));
1719
1720 let offset = state.logical_scroll_top();
1722 assert_eq!(offset.item_ix, 0);
1723 assert_eq!(offset.offset_in_item, px(0.));
1724
1725 state.scroll_by(px(0.));
1727 let offset = state.logical_scroll_top();
1728 assert_eq!(offset.item_ix, 0);
1729 assert_eq!(offset.offset_in_item, px(0.));
1730 }
1731
1732 #[open_gpui::test]
1733 fn test_child_wheel_handler_prevents_parent_list_scroll(cx: &mut TestAppContext) {
1734 let cx = cx.add_empty_window();
1735
1736 let state = ListState::new(5, crate::ListAlignment::Top, px(0.));
1737
1738 struct TestView(ListState);
1739 impl Render for TestView {
1740 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1741 list(self.0.clone(), |ix, _, _| {
1742 div()
1743 .h(px(80.))
1744 .w_full()
1745 .when(ix == 0, |this| {
1746 this.child(div().h(px(60.)).w_full().on_scroll_wheel(
1747 |_, window, cx| {
1748 window.prevent_default();
1749 cx.stop_propagation();
1750 },
1751 ))
1752 })
1753 .into_any()
1754 })
1755 .w_full()
1756 .h_full()
1757 }
1758 }
1759
1760 cx.draw(point(px(0.), px(0.)), size(px(100.), px(40.)), |_, cx| {
1761 cx.new(|_| TestView(state.clone())).into_any_element()
1762 });
1763
1764 cx.simulate_event(ScrollWheelEvent {
1765 position: point(px(10.), px(10.)),
1766 delta: ScrollDelta::Pixels(point(px(0.), px(-80.))),
1767 ..Default::default()
1768 });
1769
1770 let offset = state.logical_scroll_top();
1771 assert_eq!(offset.item_ix, 0);
1772 assert_eq!(offset.offset_in_item, px(0.));
1773 }
1774
1775 struct TestListView(ListState);
1776 impl Render for TestListView {
1777 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1778 list(self.0.clone(), |_, _, _| {
1779 div().h(px(20.)).w_full().into_any()
1780 })
1781 .w_full()
1782 .h_full()
1783 }
1784 }
1785
1786 #[open_gpui::test]
1787 fn test_item_viewport_queries_return_none_before_layout(_cx: &mut TestAppContext) {
1788 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1789
1790 assert_eq!(state.item_is_above_viewport(0), None);
1791 assert_eq!(state.item_is_below_viewport(0), None);
1792 }
1793
1794 #[open_gpui::test]
1795 fn test_item_viewport_queries_before_logical_scroll_top(cx: &mut TestAppContext) {
1796 let cx = cx.add_empty_window();
1797
1798 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1799
1800 state.scroll_to(open_gpui::ListOffset {
1801 item_ix: 2,
1802 offset_in_item: px(0.),
1803 });
1804 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1805 cx.new(|_| TestListView(state.clone())).into_any_element()
1806 });
1807
1808 assert_eq!(state.item_is_above_viewport(1), Some(true));
1809 assert_eq!(state.item_is_below_viewport(1), Some(false));
1810 }
1811
1812 #[open_gpui::test]
1813 fn test_item_viewport_queries_measured_item_inside_viewport(cx: &mut TestAppContext) {
1814 let cx = cx.add_empty_window();
1815
1816 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1817
1818 state.scroll_to(open_gpui::ListOffset {
1819 item_ix: 2,
1820 offset_in_item: px(0.),
1821 });
1822 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1823 cx.new(|_| TestListView(state.clone())).into_any_element()
1824 });
1825
1826 assert_eq!(state.item_is_above_viewport(2), Some(false));
1827 assert_eq!(state.item_is_below_viewport(2), Some(false));
1828 }
1829
1830 #[open_gpui::test]
1831 fn test_item_viewport_queries_measured_item_above_viewport(cx: &mut TestAppContext) {
1832 let cx = cx.add_empty_window();
1833
1834 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1835
1836 state.scroll_to(open_gpui::ListOffset {
1837 item_ix: 2,
1838 offset_in_item: px(20.),
1839 });
1840 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1841 cx.new(|_| TestListView(state.clone())).into_any_element()
1842 });
1843
1844 assert_eq!(state.item_is_above_viewport(2), Some(true));
1845 assert_eq!(state.item_is_below_viewport(2), Some(false));
1846 }
1847
1848 #[open_gpui::test]
1849 fn test_item_viewport_queries_measured_item_below_viewport(cx: &mut TestAppContext) {
1850 let cx = cx.add_empty_window();
1851
1852 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1853
1854 state.scroll_to(open_gpui::ListOffset {
1855 item_ix: 2,
1856 offset_in_item: px(0.),
1857 });
1858 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1859 cx.new(|_| TestListView(state.clone())).into_any_element()
1860 });
1861
1862 assert_eq!(state.item_is_above_viewport(3), Some(false));
1863 assert_eq!(state.item_is_below_viewport(3), Some(true));
1864 }
1865
1866 #[open_gpui::test]
1867 fn test_item_viewport_queries_remain_stable_with_zero_height_viewport(cx: &mut TestAppContext) {
1868 let cx = cx.add_empty_window();
1869
1870 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1871
1872 state.scroll_to(open_gpui::ListOffset {
1873 item_ix: 2,
1874 offset_in_item: px(0.),
1875 });
1876 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1877 cx.new(|_| TestListView(state.clone())).into_any_element()
1878 });
1879
1880 assert_eq!(state.item_is_above_viewport(3), Some(false));
1881 assert_eq!(state.item_is_below_viewport(3), Some(true));
1882
1883 cx.draw(point(px(0.), px(0.)), size(px(100.), px(0.)), |_, cx| {
1888 cx.new(|_| TestListView(state.clone())).into_any_element()
1889 });
1890
1891 assert_eq!(state.item_is_above_viewport(1), Some(true));
1892 assert_eq!(state.item_is_below_viewport(1), Some(false));
1893 assert_eq!(state.item_is_above_viewport(3), Some(false));
1894 assert_eq!(state.item_is_below_viewport(3), Some(true));
1895 }
1896
1897 #[open_gpui::test]
1898 fn test_item_viewport_queries_after_scroll_to_end_before_layout(cx: &mut TestAppContext) {
1899 let cx = cx.add_empty_window();
1900
1901 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1902
1903 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1904 cx.new(|_| TestListView(state.clone())).into_any_element()
1905 });
1906
1907 state.scroll_to_end();
1908
1909 assert_eq!(state.logical_scroll_top().item_ix, state.item_count());
1910 assert_eq!(state.item_is_above_viewport(0), Some(true));
1911 assert_eq!(state.item_is_below_viewport(0), Some(false));
1912 }
1913
1914 #[open_gpui::test]
1915 fn test_measure_all_after_width_change(cx: &mut TestAppContext) {
1916 let cx = cx.add_empty_window();
1917
1918 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
1919
1920 struct TestView(ListState);
1921 impl Render for TestView {
1922 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1923 list(self.0.clone(), |_, _, _| {
1924 div().h(px(50.)).w_full().into_any()
1925 })
1926 .w_full()
1927 .h_full()
1928 }
1929 }
1930
1931 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
1932
1933 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1936 view.clone().into_any_element()
1937 });
1938 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
1939
1940 cx.draw(point(px(0.), px(0.)), size(px(200.), px(200.)), |_, _| {
1944 view.into_any_element()
1945 });
1946 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
1947 }
1948
1949 #[open_gpui::test]
1950 fn test_remeasure(cx: &mut TestAppContext) {
1951 let cx = cx.add_empty_window();
1952
1953 let item_height = Rc::new(Cell::new(100usize));
1957 let state = ListState::new(10, crate::ListAlignment::Top, px(10.));
1958
1959 struct TestView {
1960 state: ListState,
1961 item_height: Rc<Cell<usize>>,
1962 }
1963
1964 impl Render for TestView {
1965 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1966 let height = self.item_height.get();
1967 list(self.state.clone(), move |_, _, _| {
1968 div().h(px(height as f32)).w_full().into_any()
1969 })
1970 .w_full()
1971 .h_full()
1972 }
1973 }
1974
1975 let state_clone = state.clone();
1976 let item_height_clone = item_height.clone();
1977 let view = cx.update(|_, cx| {
1978 cx.new(|_| TestView {
1979 state: state_clone,
1980 item_height: item_height_clone,
1981 })
1982 });
1983
1984 state.scroll_to(open_gpui::ListOffset {
1987 item_ix: 2,
1988 offset_in_item: px(40.),
1989 });
1990
1991 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1992 view.clone().into_any_element()
1993 });
1994
1995 let offset = state.logical_scroll_top();
1996 assert_eq!(offset.item_ix, 2);
1997 assert_eq!(offset.offset_in_item, px(40.));
1998
1999 item_height.set(50);
2004 state.remeasure();
2005
2006 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2007 view.into_any_element()
2008 });
2009
2010 let offset = state.logical_scroll_top();
2011 assert_eq!(offset.item_ix, 2);
2012 assert_eq!(offset.offset_in_item, px(20.));
2013 }
2014
2015 #[open_gpui::test]
2016 fn test_remeasure_item_preserves_scroll_offset(cx: &mut TestAppContext) {
2017 let cx = cx.add_empty_window();
2018
2019 let item_height = Rc::new(Cell::new(100usize));
2020 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2021
2022 struct TestView {
2023 state: ListState,
2024 item_height: Rc<Cell<usize>>,
2025 }
2026
2027 impl Render for TestView {
2028 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2029 let height = self.item_height.get();
2030 list(self.state.clone(), move |index, _, _| {
2031 let height = if index == 5 { height } else { 100 };
2032 div().h(px(height as f32)).w_full().into_any()
2033 })
2034 .w_full()
2035 .h_full()
2036 }
2037 }
2038
2039 let state_clone = state.clone();
2040 let item_height_clone = item_height.clone();
2041 let view = cx.update(|_, cx| {
2042 cx.new(|_| TestView {
2043 state: state_clone,
2044 item_height: item_height_clone,
2045 })
2046 });
2047
2048 state.scroll_to(open_gpui::ListOffset {
2049 item_ix: 5,
2050 offset_in_item: px(40.),
2051 });
2052
2053 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2054 view.clone().into_any_element()
2055 });
2056
2057 item_height.set(200);
2058 state.remeasure_items(5..6);
2059
2060 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2061 view.into_any_element()
2062 });
2063
2064 let offset = state.logical_scroll_top();
2065 assert_eq!(offset.item_ix, 5);
2066 assert_eq!(offset.offset_in_item, px(40.));
2067 }
2068
2069 #[open_gpui::test]
2070 fn test_remeasure_then_scroll_does_not_revert_scroll_position(cx: &mut TestAppContext) {
2071 let cx = cx.add_empty_window();
2072
2073 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2074
2075 struct TestView(ListState);
2076 impl Render for TestView {
2077 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2078 list(self.0.clone(), |_, _, _| {
2079 div().h(px(100.)).w_full().into_any()
2080 })
2081 .w_full()
2082 .h_full()
2083 }
2084 }
2085
2086 let view = {
2087 let state = state.clone();
2088 cx.update(|_, cx| cx.new(|_| TestView(state)))
2089 };
2090
2091 state.scroll_to(open_gpui::ListOffset {
2092 item_ix: 5,
2093 offset_in_item: px(40.),
2094 });
2095
2096 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2097 view.clone().into_any_element()
2098 });
2099
2100 state.remeasure_items(5..6);
2101
2102 cx.simulate_event(ScrollWheelEvent {
2103 position: point(px(50.), px(100.)),
2104 delta: ScrollDelta::Pixels(point(px(0.), px(-30.))),
2105 ..Default::default()
2106 });
2107
2108 let offset = state.logical_scroll_top();
2109 assert_eq!(offset.item_ix, 5);
2110 assert_eq!(offset.offset_in_item, px(70.));
2111
2112 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2113 view.into_any_element()
2114 });
2115
2116 let offset = state.logical_scroll_top();
2117 assert_eq!(offset.item_ix, 5);
2118 assert_eq!(
2119 offset.offset_in_item,
2120 px(70.),
2121 "scrolling after a remeasure should not be reverted by the stale pending scroll"
2122 );
2123 }
2124
2125 #[open_gpui::test]
2126 fn test_scroll_after_remeasure_clamps_to_shrunk_item_height(cx: &mut TestAppContext) {
2127 let cx = cx.add_empty_window();
2128
2129 let item_height = Rc::new(Cell::new(100usize));
2130 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2131
2132 struct TestView {
2133 state: ListState,
2134 item_height: Rc<Cell<usize>>,
2135 }
2136
2137 impl Render for TestView {
2138 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2139 let height = self.item_height.get();
2140 list(self.state.clone(), move |index, _, _| {
2141 let height = if index == 5 { height } else { 100 };
2142 div().h(px(height as f32)).w_full().into_any()
2143 })
2144 .w_full()
2145 .h_full()
2146 }
2147 }
2148
2149 let view = {
2150 let state = state.clone();
2151 let item_height = item_height.clone();
2152 cx.update(|_, cx| cx.new(|_| TestView { state, item_height }))
2153 };
2154
2155 state.scroll_to(open_gpui::ListOffset {
2156 item_ix: 5,
2157 offset_in_item: px(40.),
2158 });
2159
2160 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2161 view.clone().into_any_element()
2162 });
2163
2164 item_height.set(50);
2165 state.remeasure_items(5..6);
2166
2167 cx.simulate_event(ScrollWheelEvent {
2168 position: point(px(50.), px(100.)),
2169 delta: ScrollDelta::Pixels(point(px(0.), px(-30.))),
2170 ..Default::default()
2171 });
2172
2173 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2174 view.into_any_element()
2175 });
2176
2177 let offset = state.logical_scroll_top();
2178 assert_eq!(offset.item_ix, 5);
2179 assert_eq!(offset.offset_in_item, px(50.));
2180 }
2181
2182 #[open_gpui::test]
2183 fn test_follow_tail_stays_at_bottom_as_items_grow(cx: &mut TestAppContext) {
2184 let cx = cx.add_empty_window();
2185
2186 let item_height = Rc::new(Cell::new(50usize));
2189 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2190
2191 struct TestView {
2192 state: ListState,
2193 item_height: Rc<Cell<usize>>,
2194 }
2195 impl Render for TestView {
2196 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2197 let height = self.item_height.get();
2198 list(self.state.clone(), move |_, _, _| {
2199 div().h(px(height as f32)).w_full().into_any()
2200 })
2201 .w_full()
2202 .h_full()
2203 }
2204 }
2205
2206 let state_clone = state.clone();
2207 let item_height_clone = item_height.clone();
2208 let view = cx.update(|_, cx| {
2209 cx.new(|_| TestView {
2210 state: state_clone,
2211 item_height: item_height_clone,
2212 })
2213 });
2214
2215 state.set_follow_mode(FollowMode::Tail);
2216
2217 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2220 view.clone().into_any_element()
2221 });
2222
2223 let offset = state.logical_scroll_top();
2226 assert_eq!(offset.item_ix, 6);
2227 assert_eq!(offset.offset_in_item, px(0.));
2228 assert!(state.is_following_tail());
2229
2230 item_height.set(80);
2233 state.remeasure();
2234
2235 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2236 view.into_any_element()
2237 });
2238
2239 let offset = state.logical_scroll_top();
2246 assert_eq!(offset.item_ix, 7);
2247 assert_eq!(offset.offset_in_item, px(40.));
2248 assert!(state.is_following_tail());
2249 }
2250
2251 #[open_gpui::test]
2252 fn test_follow_tail_disengages_on_user_scroll(cx: &mut TestAppContext) {
2253 let cx = cx.add_empty_window();
2254
2255 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2257
2258 struct TestView(ListState);
2259 impl Render for TestView {
2260 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2261 list(self.0.clone(), |_, _, _| {
2262 div().h(px(50.)).w_full().into_any()
2263 })
2264 .w_full()
2265 .h_full()
2266 }
2267 }
2268
2269 state.set_follow_mode(FollowMode::Tail);
2270
2271 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, cx| {
2273 cx.new(|_| TestView(state.clone())).into_any_element()
2274 });
2275 assert!(state.is_following_tail());
2276
2277 cx.simulate_event(ScrollWheelEvent {
2280 position: point(px(50.), px(100.)),
2281 delta: ScrollDelta::Pixels(point(px(0.), px(100.))),
2282 ..Default::default()
2283 });
2284
2285 assert!(
2286 !state.is_following_tail(),
2287 "follow-tail should disengage when the user scrolls toward the start"
2288 );
2289 }
2290
2291 #[open_gpui::test]
2292 fn test_follow_tail_disengages_on_scrollbar_reposition(cx: &mut TestAppContext) {
2293 let cx = cx.add_empty_window();
2294
2295 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
2297
2298 struct TestView(ListState);
2299 impl Render for TestView {
2300 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2301 list(self.0.clone(), |_, _, _| {
2302 div().h(px(50.)).w_full().into_any()
2303 })
2304 .w_full()
2305 .h_full()
2306 }
2307 }
2308
2309 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2310
2311 state.set_follow_mode(FollowMode::Tail);
2312
2313 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2315 view.clone().into_any_element()
2316 });
2317 assert!(state.is_following_tail());
2318
2319 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
2321
2322 let offset = state.logical_scroll_top();
2323 assert_eq!(offset.item_ix, 3);
2324 assert_eq!(offset.offset_in_item, px(0.));
2325 assert!(
2326 !state.is_following_tail(),
2327 "follow-tail should disengage when the scrollbar manually repositions the list"
2328 );
2329
2330 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2333 view.into_any_element()
2334 });
2335
2336 let offset = state.logical_scroll_top();
2337 assert_eq!(offset.item_ix, 3);
2338 assert_eq!(offset.offset_in_item, px(0.));
2339 }
2340
2341 #[open_gpui::test]
2342 fn test_scrollbar_drag_with_growing_content(cx: &mut TestAppContext) {
2343 let cx = cx.add_empty_window();
2344
2345 let last_item_height = Rc::new(Cell::new(50usize));
2346 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
2347
2348 struct TestView {
2349 state: ListState,
2350 last_item_height: Rc<Cell<usize>>,
2351 }
2352 impl Render for TestView {
2353 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2354 let last_item_height = self.last_item_height.clone();
2355 list(self.state.clone(), move |index, _, _| {
2356 let height = if index == 9 {
2357 last_item_height.get()
2358 } else {
2359 50
2360 };
2361 div().h(px(height as f32)).w_full().into_any()
2362 })
2363 .w_full()
2364 .h_full()
2365 }
2366 }
2367
2368 let view = cx.update(|_, cx| {
2369 cx.new(|_| TestView {
2370 state: state.clone(),
2371 last_item_height: last_item_height.clone(),
2372 })
2373 });
2374
2375 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2376 view.clone().into_any_element()
2377 });
2378
2379 state.scrollbar_drag_started();
2380
2381 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
2382 let scrollbar_offset_before_growth = state.scroll_px_offset_for_scrollbar();
2383
2384 let offset = state.logical_scroll_top();
2385 assert_eq!(offset.item_ix, 3);
2386 assert_eq!(offset.offset_in_item, px(0.));
2387
2388 last_item_height.set(550);
2389 state.remeasure_items(9..10);
2390 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2391 view.clone().into_any_element()
2392 });
2393
2394 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
2395 assert_eq!(
2396 state.scroll_px_offset_for_scrollbar(),
2397 scrollbar_offset_before_growth
2398 );
2399
2400 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
2401 let offset = state.logical_scroll_top();
2402 assert_eq!(offset.item_ix, 3);
2403 assert_eq!(offset.offset_in_item, px(0.));
2404 }
2405
2406 #[open_gpui::test]
2407 fn test_set_follow_tail_snaps_to_bottom(cx: &mut TestAppContext) {
2408 let cx = cx.add_empty_window();
2409
2410 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2412
2413 struct TestView(ListState);
2414 impl Render for TestView {
2415 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2416 list(self.0.clone(), |_, _, _| {
2417 div().h(px(50.)).w_full().into_any()
2418 })
2419 .w_full()
2420 .h_full()
2421 }
2422 }
2423
2424 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2425
2426 state.scroll_to(open_gpui::ListOffset {
2428 item_ix: 3,
2429 offset_in_item: px(0.),
2430 });
2431
2432 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2433 view.clone().into_any_element()
2434 });
2435
2436 let offset = state.logical_scroll_top();
2437 assert_eq!(offset.item_ix, 3);
2438 assert_eq!(offset.offset_in_item, px(0.));
2439 assert!(!state.is_following_tail());
2440
2441 state.set_follow_mode(FollowMode::Tail);
2444
2445 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2446 view.into_any_element()
2447 });
2448
2449 let offset = state.logical_scroll_top();
2452 assert_eq!(offset.item_ix, 6);
2453 assert_eq!(offset.offset_in_item, px(0.));
2454 assert!(state.is_following_tail());
2455 }
2456
2457 #[open_gpui::test]
2458 fn test_bottom_aligned_scrollbar_offset_at_end(cx: &mut TestAppContext) {
2459 let cx = cx.add_empty_window();
2460
2461 const ITEMS: usize = 10;
2462 const ITEM_SIZE: f32 = 50.0;
2463
2464 let state = ListState::new(
2465 ITEMS,
2466 crate::ListAlignment::Bottom,
2467 px(ITEMS as f32 * ITEM_SIZE),
2468 );
2469
2470 struct TestView(ListState);
2471 impl Render for TestView {
2472 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2473 list(self.0.clone(), |_, _, _| {
2474 div().h(px(ITEM_SIZE)).w_full().into_any()
2475 })
2476 .w_full()
2477 .h_full()
2478 }
2479 }
2480
2481 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
2482 cx.new(|_| TestView(state.clone())).into_any_element()
2483 });
2484
2485 assert_eq!(state.logical_scroll_top().item_ix, ITEMS);
2488
2489 let max_offset = state.max_offset_for_scrollbar();
2490 let scroll_offset = state.scroll_px_offset_for_scrollbar();
2491
2492 assert_eq!(
2493 -scroll_offset.y, max_offset.y,
2494 "scrollbar offset ({}) should equal max offset ({}) when list is pinned to bottom",
2495 -scroll_offset.y, max_offset.y,
2496 );
2497 }
2498
2499 #[open_gpui::test]
2503 fn test_follow_tail_reengages_when_scrolled_back_to_bottom(cx: &mut TestAppContext) {
2504 let cx = cx.add_empty_window();
2505
2506 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2508
2509 struct TestView(ListState);
2510 impl Render for TestView {
2511 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2512 list(self.0.clone(), |_, _, _| {
2513 div().h(px(50.)).w_full().into_any()
2514 })
2515 .w_full()
2516 .h_full()
2517 }
2518 }
2519
2520 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2521
2522 state.set_follow_mode(FollowMode::Tail);
2523
2524 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2525 view.clone().into_any_element()
2526 });
2527 assert!(state.is_following_tail());
2528
2529 cx.simulate_event(ScrollWheelEvent {
2531 position: point(px(50.), px(100.)),
2532 delta: ScrollDelta::Pixels(point(px(0.), px(50.))),
2533 ..Default::default()
2534 });
2535 assert!(!state.is_following_tail());
2536
2537 cx.simulate_event(ScrollWheelEvent {
2539 position: point(px(50.), px(100.)),
2540 delta: ScrollDelta::Pixels(point(px(0.), px(-10000.))),
2541 ..Default::default()
2542 });
2543
2544 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2547 view.clone().into_any_element()
2548 });
2549 assert!(
2550 state.is_following_tail(),
2551 "follow_tail should re-engage after scrolling back to the bottom"
2552 );
2553 }
2554
2555 #[open_gpui::test]
2558 fn test_follow_tail_reengagement_not_fooled_by_unmeasured_items(cx: &mut TestAppContext) {
2559 let cx = cx.add_empty_window();
2560
2561 let state = ListState::new(20, crate::ListAlignment::Top, px(1000.));
2565
2566 struct TestView(ListState);
2567 impl Render for TestView {
2568 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2569 list(self.0.clone(), |_, _, _| {
2570 div().h(px(50.)).w_full().into_any()
2571 })
2572 .w_full()
2573 .h_full()
2574 }
2575 }
2576
2577 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2578
2579 state.set_follow_mode(FollowMode::Tail);
2580
2581 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2582 view.clone().into_any_element()
2583 });
2584 assert!(state.is_following_tail());
2585
2586 cx.simulate_event(ScrollWheelEvent {
2590 position: point(px(50.), px(100.)),
2591 delta: ScrollDelta::Pixels(point(px(0.), px(200.))),
2592 ..Default::default()
2593 });
2594 assert!(!state.is_following_tail());
2595
2596 state.remeasure_items(19..20);
2600
2601 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2607 view.clone().into_any_element()
2608 });
2609 assert!(
2610 !state.is_following_tail(),
2611 "follow_tail should not falsely re-engage due to an unmeasured item \
2612 reducing items.summary().height"
2613 );
2614 }
2615
2616 #[open_gpui::test]
2617 fn test_follow_tail_reengages_after_scrollbar_disengagement(cx: &mut TestAppContext) {
2618 let cx = cx.add_empty_window();
2619
2620 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
2622
2623 struct TestView(ListState);
2624 impl Render for TestView {
2625 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2626 list(self.0.clone(), |_, _, _| {
2627 div().h(px(50.)).w_full().into_any()
2628 })
2629 .w_full()
2630 .h_full()
2631 }
2632 }
2633
2634 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2635
2636 state.set_follow_mode(FollowMode::Tail);
2637 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2638 view.clone().into_any_element()
2639 });
2640 assert!(state.is_following_tail());
2641
2642 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
2644 assert!(!state.is_following_tail());
2645
2646 state.set_offset_from_scrollbar(point(px(0.), px(-300.)));
2649 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2650 view.into_any_element()
2651 });
2652 assert!(
2653 state.is_following_tail(),
2654 "follow_tail should re-engage after scrolling back to the bottom via the scrollbar"
2655 );
2656 }
2657
2658 #[open_gpui::test]
2659 fn test_follow_tail_reengages_after_scrollbar_drag_to_bottom_while_growing(
2660 cx: &mut TestAppContext,
2661 ) {
2662 let cx = cx.add_empty_window();
2663
2664 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
2665
2666 struct TestView(ListState);
2667 impl Render for TestView {
2668 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2669 list(self.0.clone(), |_, _, _| {
2670 div().h(px(50.)).w_full().into_any()
2671 })
2672 .w_full()
2673 .h_full()
2674 }
2675 }
2676
2677 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2678
2679 state.set_follow_mode(FollowMode::Tail);
2680 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2681 view.clone().into_any_element()
2682 });
2683 assert!(state.is_following_tail());
2684
2685 state.scrollbar_drag_started();
2686
2687 state.splice(10..10, 10);
2688 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2689 view.clone().into_any_element()
2690 });
2691
2692 state.set_offset_from_scrollbar(point(px(0.), px(-300.)));
2693 state.scrollbar_drag_ended();
2694
2695 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2696 view.into_any_element()
2697 });
2698
2699 assert!(
2700 state.is_following_tail(),
2701 "follow_tail should re-engage when the user drags the scrollbar to \
2702 the bottom of its track, even when content has grown during the drag \
2703 (so frozen_bottom < live_bottom)"
2704 );
2705 }
2706}