1use crate::{
17 AnyElement, App, AppContext, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges,
18 Element, EntityId, FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId,
19 IntoElement, Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style,
20 StyleRefinement, Styled, Window, point, px, size,
21};
22use collections::VecDeque;
23use refineable::Refineable as _;
24use std::{cell::RefCell, ops::Range, rc::Rc};
25use sum_tree::{Bias, Dimensions, SumTree};
26
27type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static;
28
29pub fn list(
76 state: ListState,
77 render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static,
78) -> List {
79 List {
80 state,
81 render_item: Box::new(render_item),
82 style: StyleRefinement::default(),
83 sizing_behavior: ListSizingBehavior::default(),
84 }
85}
86
87pub struct List {
89 state: ListState,
90 render_item: Box<RenderItemFn>,
91 style: StyleRefinement,
92 sizing_behavior: ListSizingBehavior,
93}
94
95impl List {
96 pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
101 self.sizing_behavior = behavior;
102 self
103 }
104}
105
106#[derive(Clone)]
112pub struct ListState(Rc<RefCell<StateInner>>);
113
114impl ListState {
115 fn state_mut(&self) -> std::cell::RefMut<'_, StateInner> {
116 let state = self.0.borrow_mut();
117 state.revision.bump();
118 state
119 }
120}
121
122impl std::fmt::Debug for ListState {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 f.write_str("ListState")
125 }
126}
127
128struct StateInner {
129 revision: crate::Revision,
131 last_layout_bounds: Option<Bounds<Pixels>>,
132 last_padding: Option<Edges<Pixels>>,
133 items: SumTree<ListItem>,
134 logical_scroll_top: Option<ListOffset>,
135 alignment: ListAlignment,
136 overdraw: Pixels,
137 reset: bool,
138 #[allow(clippy::type_complexity)]
139 scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,
140 scrollbar_drag_start_height: Option<Pixels>,
141 measuring_behavior: ListMeasuringBehavior,
142 pending_scroll: Option<PendingScroll>,
143 follow_state: FollowState,
144}
145
146#[derive(Clone)]
153enum PendingScroll {
154 Absolute { item_ix: usize, offset: Pixels },
156 Proportional(PendingScrollFraction),
158}
159
160#[derive(Clone)]
163struct PendingScrollFraction {
164 item_ix: usize,
166 fraction: f32,
168}
169
170enum ScrollAnchor {
173 Absolute,
175 Proportional,
177}
178
179#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
183pub enum FollowMode {
184 #[default]
186 Normal,
187 Tail,
189}
190
191#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
192enum FollowState {
193 #[default]
194 Normal,
195 Tail {
196 is_following: bool,
197 },
198}
199
200impl FollowState {
201 fn is_following(&self) -> bool {
202 matches!(self, FollowState::Tail { is_following: true })
203 }
204
205 fn has_stopped_following(&self) -> bool {
206 matches!(
207 self,
208 FollowState::Tail {
209 is_following: false
210 }
211 )
212 }
213
214 fn start_following(&mut self) {
215 if let FollowState::Tail {
216 is_following: false,
217 } = self
218 {
219 *self = FollowState::Tail { is_following: true };
220 }
221 }
222
223 fn stop_following(&mut self) {
224 if let FollowState::Tail { is_following: true } = self {
225 *self = FollowState::Tail {
226 is_following: false,
227 };
228 }
229 }
230}
231
232#[derive(Clone, Copy, Debug, Eq, PartialEq)]
234pub enum ListAlignment {
235 Top,
237 Bottom,
240}
241
242pub struct ListScrollEvent {
246 pub visible_range: Range<usize>,
248
249 pub count: usize,
251
252 pub is_scrolled: bool,
255
256 pub is_following_tail: bool,
258}
259
260#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
262pub enum ListSizingBehavior {
263 Infer,
265 #[default]
267 Auto,
268}
269
270#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
274pub enum ListMeasuringBehavior {
275 Measure(bool),
278 #[default]
280 Visible,
281}
282
283impl ListMeasuringBehavior {
284 fn reset(&mut self) {
285 match self {
286 ListMeasuringBehavior::Measure(has_measured) => *has_measured = false,
287 ListMeasuringBehavior::Visible => {}
288 }
289 }
290}
291
292#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
296pub enum ListHorizontalSizingBehavior {
297 #[default]
299 FitList,
300 Unconstrained,
303}
304
305struct LayoutItemsResponse {
306 max_item_width: Pixels,
307 scroll_top: ListOffset,
308 item_layouts: VecDeque<ItemLayout>,
309}
310
311struct ItemLayout {
312 index: usize,
313 element: AnyElement,
314 size: Size<Pixels>,
315}
316
317pub struct ListPrepaintState {
319 hitbox: Hitbox,
320 layout: LayoutItemsResponse,
321}
322
323#[derive(Clone)]
324enum ListItem {
325 Unmeasured {
326 size_hint: Option<Size<Pixels>>,
327 focus_handle: Option<FocusHandle>,
328 },
329 Measured {
330 size: Size<Pixels>,
331 focus_handle: Option<FocusHandle>,
332 },
333}
334
335impl ListItem {
336 fn size(&self) -> Option<Size<Pixels>> {
337 if let ListItem::Measured { size, .. } = self {
338 Some(*size)
339 } else {
340 None
341 }
342 }
343
344 fn size_hint(&self) -> Option<Size<Pixels>> {
345 match self {
346 ListItem::Measured { size, .. } => Some(*size),
347 ListItem::Unmeasured { size_hint, .. } => *size_hint,
348 }
349 }
350
351 fn focus_handle(&self) -> Option<FocusHandle> {
352 match self {
353 ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => {
354 focus_handle.clone()
355 }
356 }
357 }
358
359 fn contains_focused(&self, window: &Window, cx: &App) -> bool {
360 match self {
361 ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => {
362 focus_handle
363 .as_ref()
364 .is_some_and(|handle| handle.contains_focused(window, cx))
365 }
366 }
367 }
368}
369
370#[derive(Clone, Debug, Default, PartialEq)]
371struct ListItemSummary {
372 count: usize,
373 rendered_count: usize,
374 unrendered_count: usize,
375 height: Pixels,
376 has_focus_handles: bool,
377 has_unknown_height: bool,
378}
379
380#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
381struct Count(usize);
382
383#[derive(Clone, Debug, Default)]
384struct Height(Pixels);
385
386impl ListState {
387 pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self {
406 let this = Self(Rc::new(RefCell::new(StateInner {
407 revision: Default::default(),
408 last_layout_bounds: None,
409 last_padding: None,
410 items: SumTree::default(),
411 logical_scroll_top: None,
412 alignment,
413 overdraw,
414 scroll_handler: None,
415 reset: false,
416 scrollbar_drag_start_height: None,
417 measuring_behavior: ListMeasuringBehavior::default(),
418 pending_scroll: None,
419 follow_state: FollowState::default(),
420 })));
421 this.splice(0..0, item_count);
422 this
423 }
424
425 pub fn measure_all(self) -> Self {
433 self.state_mut().measuring_behavior = ListMeasuringBehavior::Measure(false);
434 self
435 }
436
437 pub fn with_uniform_item_height(self, height: Pixels) -> Self {
451 self.apply_uniform_item_height(height);
452 self
453 }
454
455 pub fn reset(&self, element_count: usize) {
472 let old_count = {
473 let state = &mut *self.state_mut();
474 state.reset = true;
475 state.measuring_behavior.reset();
476 state.logical_scroll_top = None;
477 state.pending_scroll = None;
478 state.scrollbar_drag_start_height = None;
479 state.items.summary().count
480 };
481
482 self.splice(0..old_count, element_count);
483 }
484
485 pub fn reset_with_uniform_height(&self, element_count: usize, height: Pixels) {
489 self.reset(element_count);
490 self.apply_uniform_item_height(height);
491 }
492
493 fn apply_uniform_item_height(&self, height: Pixels) {
494 let size_hint = Size {
495 width: px(0.),
496 height,
497 };
498 let mut state = self.state_mut();
499 let new_items = state
500 .items
501 .iter()
502 .map(|item| ListItem::Unmeasured {
503 size_hint: Some(item.size_hint().unwrap_or(size_hint)),
504 focus_handle: item.focus_handle(),
505 })
506 .collect::<Vec<_>>();
507 let mut tree = SumTree::default();
508 tree.extend(new_items, ());
509 state.items = tree;
510 }
511
512 pub fn remeasure(&self) {
518 let count = self.item_count();
519 self.remeasure_items_with_scroll_anchor(0..count, ScrollAnchor::Proportional);
520 }
521
522 pub fn remeasure_items(&self, range: Range<usize>) {
529 self.remeasure_items_with_scroll_anchor(range, ScrollAnchor::Absolute);
530 }
531
532 fn remeasure_items_with_scroll_anchor(&self, range: Range<usize>, scroll_anchor: ScrollAnchor) {
533 let state = &mut *self.state_mut();
534
535 if let Some(scroll_top) = state.logical_scroll_top {
536 if range.contains(&scroll_top.item_ix) {
537 state.pending_scroll = match scroll_anchor {
538 ScrollAnchor::Absolute => Some(PendingScroll::Absolute {
539 item_ix: scroll_top.item_ix,
540 offset: scroll_top.offset_in_item,
541 }),
542 ScrollAnchor::Proportional => {
543 let mut cursor = state.items.cursor::<Count>(());
548 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
549
550 cursor
551 .item()
552 .and_then(|item| {
553 item.size().map(|size| {
554 let fraction = if size.height.0 > 0.0 {
555 (scroll_top.offset_in_item.0 / size.height.0)
556 .clamp(0.0, 1.0)
557 } else {
558 0.0
559 };
560
561 PendingScroll::Proportional(PendingScrollFraction {
562 item_ix: scroll_top.item_ix,
563 fraction,
564 })
565 })
566 })
567 .or_else(|| state.pending_scroll.clone())
568 }
569 };
570 }
571 }
572
573 let new_items = {
576 let mut cursor = state.items.cursor::<Count>(());
577 let mut new_items = cursor.slice(&Count(range.start), Bias::Right);
578 let invalidated = cursor.slice(&Count(range.end), Bias::Right);
579 new_items.extend(
580 invalidated.iter().map(|item| ListItem::Unmeasured {
581 size_hint: item.size_hint(),
582 focus_handle: item.focus_handle(),
583 }),
584 (),
585 );
586 new_items.append(cursor.suffix(), ());
587 new_items
588 };
589 state.items = new_items;
590 state.measuring_behavior.reset();
591 }
592
593 pub fn item_count(&self) -> usize {
603 self.0.borrow().items.summary().count
604 }
605
606 pub fn is_scrolled_to_end(&self) -> Option<bool> {
620 let state = self.0.borrow();
621 let bounds = state.last_layout_bounds?;
622 let summary = state.items.summary();
623 if summary.has_unknown_height {
624 return None;
625 }
626 let padding = state.last_padding.unwrap_or_default();
627 let content_height = summary.height + padding.top + padding.bottom;
628 let scroll_max = (content_height - bounds.size.height).max(px(0.));
629 if scroll_max <= px(0.) {
630 return None;
631 }
632 let scroll_top = state.scroll_top(&state.logical_scroll_top());
633 Some(scroll_top >= scroll_max)
634 }
635
636 pub fn splice(&self, old_range: Range<usize>, count: usize) {
654 self.splice_focusable(old_range, (0..count).map(|_| None))
655 }
656
657 pub fn splice_focusable(
663 &self,
664 old_range: Range<usize>,
665 focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
666 ) {
667 let state = &mut *self.state_mut();
668
669 let mut old_items = state.items.cursor::<Count>(());
670 let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right);
671 old_items.seek_forward(&Count(old_range.end), Bias::Right);
672
673 let mut spliced_count = 0;
674 new_items.extend(
675 focus_handles.into_iter().map(|focus_handle| {
676 spliced_count += 1;
677 ListItem::Unmeasured {
678 size_hint: None,
679 focus_handle,
680 }
681 }),
682 (),
683 );
684 new_items.append(old_items.suffix(), ());
685 drop(old_items);
686 state.items = new_items;
687
688 if let Some(ListOffset {
689 item_ix,
690 offset_in_item,
691 }) = state.logical_scroll_top.as_mut()
692 {
693 if old_range.contains(item_ix) {
694 *item_ix = old_range.start;
695 *offset_in_item = px(0.);
696 } else if old_range.end <= *item_ix {
697 *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count;
698 }
699 }
700 }
701
702 pub fn set_scroll_handler(
746 &self,
747 handler: impl FnMut(&ListScrollEvent, &mut Window, &mut App) + 'static,
748 ) {
749 self.state_mut().scroll_handler = Some(Box::new(handler))
750 }
751
752 pub fn logical_scroll_top(&self) -> ListOffset {
758 self.0.borrow().logical_scroll_top()
759 }
760
761 pub fn scroll_by(&self, distance: Pixels) {
802 if distance == px(0.) {
803 return;
804 }
805
806 let current_offset = self.logical_scroll_top();
807 let state = &mut *self.state_mut();
808
809 if distance < px(0.) {
810 state.follow_state.stop_following();
811 }
812
813 let mut cursor = state.items.cursor::<ListItemSummary>(());
814 cursor.seek(&Count(current_offset.item_ix), Bias::Right);
815
816 let start_pixel_offset = cursor.start().height + current_offset.offset_in_item;
817 let new_pixel_offset = (start_pixel_offset + distance).max(px(0.));
818 if new_pixel_offset > start_pixel_offset {
819 cursor.seek_forward(&Height(new_pixel_offset), Bias::Right);
820 } else {
821 cursor.seek(&Height(new_pixel_offset), Bias::Right);
822 }
823
824 let scroll_top = ListOffset {
825 item_ix: cursor.start().count,
826 offset_in_item: new_pixel_offset - cursor.start().height,
827 };
828 drop(cursor);
829 state.rebase_pending_scroll(scroll_top);
830 state.logical_scroll_top = Some(scroll_top);
831 }
832
833 pub fn scroll_to_end(&self) {
839 let state = &mut *self.state_mut();
840 let item_count = state.items.summary().count;
841 state.pending_scroll = None;
842 state.logical_scroll_top = Some(ListOffset {
843 item_ix: item_count,
844 offset_in_item: px(0.),
845 });
846 }
847
848 pub fn set_follow_mode(&self, mode: FollowMode) {
866 let state = &mut *self.state_mut();
867
868 match mode {
869 FollowMode::Normal => {
870 state.follow_state = FollowState::Normal;
871 }
872 FollowMode::Tail => {
873 state.follow_state = FollowState::Tail { is_following: true };
874 if matches!(mode, FollowMode::Tail) {
875 let item_count = state.items.summary().count;
876 state.logical_scroll_top = Some(ListOffset {
877 item_ix: item_count,
878 offset_in_item: px(0.),
879 });
880 }
881 }
882 }
883 }
884
885 pub fn pause_following_tail(&self) {
904 self.state_mut().follow_state.stop_following();
905 }
906
907 pub fn is_following_tail(&self) -> bool {
913 matches!(
914 self.0.borrow().follow_state,
915 FollowState::Tail { is_following: true }
916 )
917 }
918
919 pub fn scroll_to(&self, mut scroll_top: ListOffset) {
934 let state = &mut *self.state_mut();
935 let item_count = state.items.summary().count;
936 if scroll_top.item_ix >= item_count {
937 scroll_top.item_ix = item_count;
938 scroll_top.offset_in_item = px(0.);
939 }
940
941 if scroll_top.item_ix < item_count {
942 state.follow_state.stop_following();
943 }
944
945 state.rebase_pending_scroll(scroll_top);
946 state.logical_scroll_top = Some(scroll_top);
947 }
948
949 pub fn scroll_to_reveal_item(&self, ix: usize) {
985 let state = &mut *self.state_mut();
986
987 let mut scroll_top = state.logical_scroll_top();
988 let height = state
989 .last_layout_bounds
990 .map_or(px(0.), |bounds| bounds.size.height);
991 let padding = state.last_padding.unwrap_or_default();
992
993 if ix <= scroll_top.item_ix {
994 scroll_top.item_ix = ix;
995 scroll_top.offset_in_item = px(0.);
996 } else {
997 let mut cursor = state.items.cursor::<ListItemSummary>(());
998 cursor.seek(&Count(ix + 1), Bias::Right);
999 let bottom = cursor.start().height + padding.top;
1000 let goal_top = px(0.).max(bottom - height + padding.bottom);
1001
1002 cursor.seek(&Height(goal_top), Bias::Left);
1003 let start_ix = cursor.start().count;
1004 let start_item_top = cursor.start().height;
1005
1006 if start_ix >= scroll_top.item_ix {
1007 scroll_top.item_ix = start_ix;
1008 scroll_top.offset_in_item = goal_top - start_item_top;
1009 }
1010 }
1011
1012 state.rebase_pending_scroll(scroll_top);
1013 state.logical_scroll_top = Some(scroll_top);
1014 }
1015
1016 pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
1023 let state = &*self.0.borrow();
1024
1025 let bounds = state.last_layout_bounds.unwrap_or_default();
1026 let scroll_top = state.logical_scroll_top();
1027 if ix < scroll_top.item_ix {
1028 return None;
1029 }
1030
1031 let mut cursor = state.items.cursor::<Dimensions<Count, Height>>(());
1032 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
1033
1034 let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item;
1035
1036 cursor.seek_forward(&Count(ix), Bias::Right);
1037 if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
1038 let &Dimensions(Count(count), Height(top), _) = cursor.start();
1039 if count == ix {
1040 let top = bounds.top() + top - scroll_top;
1041 return Some(Bounds::from_corners(
1042 point(bounds.left(), top),
1043 point(bounds.right(), top + size.height),
1044 ));
1045 }
1046 }
1047 None
1048 }
1049
1050 pub fn scrollbar_drag_started(&self) {
1055 let mut state = self.state_mut();
1056 state.scrollbar_drag_start_height = Some(state.items.summary().height);
1057 }
1058
1059 pub fn scrollbar_drag_ended(&self) {
1062 self.state_mut().scrollbar_drag_start_height.take();
1063 }
1064
1065 pub fn is_scrollbar_dragging(&self) -> bool {
1071 self.0.borrow().scrollbar_drag_start_height.is_some()
1072 }
1073
1074 pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
1080 self.state_mut().set_offset_from_scrollbar(point);
1081 }
1082
1083 pub fn max_offset_for_scrollbar(&self) -> Point<Pixels> {
1088 let state = self.0.borrow();
1089 point(Pixels::ZERO, state.max_scroll_offset())
1090 }
1091
1092 pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
1097 let state = &self.0.borrow();
1098
1099 if state.logical_scroll_top.is_none() && state.alignment == ListAlignment::Bottom {
1100 return Point::new(px(0.), -state.max_scroll_offset());
1101 }
1102
1103 let logical_scroll_top = state.logical_scroll_top();
1104
1105 let mut cursor = state.items.cursor::<ListItemSummary>(());
1106 let summary: ListItemSummary =
1107 cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right);
1108 let offset = summary.height + logical_scroll_top.offset_in_item;
1109
1110 Point::new(px(0.), -offset)
1111 }
1112
1113 pub fn viewport_bounds(&self) -> Bounds<Pixels> {
1117 self.0.borrow().last_layout_bounds.unwrap_or_default()
1118 }
1119
1120 pub fn item_is_above_viewport(&self, ix: usize) -> Option<bool> {
1127 let viewport_bounds = self.0.borrow().last_layout_bounds?;
1128
1129 let scroll_top = self.logical_scroll_top();
1130 if ix < scroll_top.item_ix {
1131 return Some(true);
1134 }
1135
1136 let item_bounds = self.bounds_for_item(ix)?;
1137 Some(item_bounds.bottom() <= viewport_bounds.top())
1138 }
1139
1140 pub fn item_is_below_viewport(&self, ix: usize) -> Option<bool> {
1145 let viewport_bounds = self.0.borrow().last_layout_bounds?;
1146
1147 let scroll_top = self.logical_scroll_top();
1148 if ix < scroll_top.item_ix {
1149 return Some(false);
1152 }
1153
1154 let item_bounds = self.bounds_for_item(ix)?;
1155 Some(item_bounds.top() >= viewport_bounds.bottom())
1156 }
1157}
1158
1159impl StateInner {
1160 fn rebase_pending_scroll(&mut self, scroll_top: ListOffset) {
1164 let Some(pending) = self.pending_scroll.take() else {
1165 return;
1166 };
1167 if scroll_top.item_ix >= self.items.summary().count {
1168 return;
1169 }
1170
1171 self.pending_scroll = match pending {
1172 PendingScroll::Absolute { .. } => Some(PendingScroll::Absolute {
1173 item_ix: scroll_top.item_ix,
1174 offset: scroll_top.offset_in_item,
1175 }),
1176 PendingScroll::Proportional(_) => {
1177 let mut cursor = self.items.cursor::<Count>(());
1178 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
1179 cursor
1180 .item()
1181 .and_then(|item| item.size_hint())
1182 .filter(|size| size.height.0 > 0.0)
1183 .map(|size| {
1184 PendingScroll::Proportional(PendingScrollFraction {
1185 item_ix: scroll_top.item_ix,
1186 fraction: (scroll_top.offset_in_item.0 / size.height.0).clamp(0.0, 1.0),
1187 })
1188 })
1189 }
1190 };
1191 }
1192
1193 fn max_scroll_offset(&self) -> Pixels {
1194 let bounds = self.last_layout_bounds.unwrap_or_default();
1195 let height = self
1196 .scrollbar_drag_start_height
1197 .unwrap_or_else(|| self.items.summary().height);
1198 (height - bounds.size.height).max(px(0.))
1199 }
1200
1201 fn visible_range(
1202 items: &SumTree<ListItem>,
1203 height: Pixels,
1204 scroll_top: &ListOffset,
1205 ) -> Range<usize> {
1206 let mut cursor = items.cursor::<ListItemSummary>(());
1207 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
1208 let start_y = cursor.start().height + scroll_top.offset_in_item;
1209 cursor.seek_forward(&Height(start_y + height), Bias::Left);
1210 scroll_top.item_ix..(cursor.start().count + 1).min(items.summary().count)
1211 }
1212
1213 fn scroll(
1214 &mut self,
1215 scroll_top: &ListOffset,
1216 height: Pixels,
1217 delta: Point<Pixels>,
1218 current_view: EntityId,
1219 window: &mut Window,
1220 cx: &mut App,
1221 ) {
1222 if self.reset {
1225 return;
1226 }
1227
1228 let padding = self.last_padding.unwrap_or_default();
1229 let scroll_max =
1230 (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
1231 let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
1232 .max(px(0.))
1233 .min(scroll_max);
1234
1235 let (start, ..) =
1236 self.items
1237 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
1238 let resulting_scroll_top = ListOffset {
1239 item_ix: start.count,
1240 offset_in_item: new_scroll_top - start.height,
1241 };
1242
1243 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
1244 self.pending_scroll = None;
1245 self.logical_scroll_top = None;
1246 } else {
1247 self.rebase_pending_scroll(resulting_scroll_top);
1251 self.logical_scroll_top = Some(resulting_scroll_top);
1252 }
1253
1254 if delta.y > px(0.) {
1255 self.follow_state.stop_following();
1256 }
1257
1258 if let Some(handler) = self.scroll_handler.as_mut() {
1259 let visible_range = Self::visible_range(&self.items, height, &resulting_scroll_top);
1260 handler(
1261 &ListScrollEvent {
1262 visible_range,
1263 count: self.items.summary().count,
1264 is_scrolled: self.logical_scroll_top.is_some(),
1265 is_following_tail: matches!(
1266 self.follow_state,
1267 FollowState::Tail { is_following: true }
1268 ),
1269 },
1270 window,
1271 cx,
1272 );
1273 }
1274
1275 cx.notify(current_view);
1276 }
1277
1278 fn logical_scroll_top(&self) -> ListOffset {
1279 self.logical_scroll_top
1280 .unwrap_or_else(|| match self.alignment {
1281 ListAlignment::Top => ListOffset {
1282 item_ix: 0,
1283 offset_in_item: px(0.),
1284 },
1285 ListAlignment::Bottom => ListOffset {
1286 item_ix: self.items.summary().count,
1287 offset_in_item: px(0.),
1288 },
1289 })
1290 }
1291
1292 fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
1293 let (start, ..) = self.items.find::<ListItemSummary, _>(
1294 (),
1295 &Count(logical_scroll_top.item_ix),
1296 Bias::Right,
1297 );
1298 start.height + logical_scroll_top.offset_in_item
1299 }
1300
1301 fn layout_all_items(
1302 &mut self,
1303 available_width: Pixels,
1304 render_item: &mut RenderItemFn,
1305 window: &mut Window,
1306 cx: &mut App,
1307 ) {
1308 match &mut self.measuring_behavior {
1309 ListMeasuringBehavior::Visible => {
1310 return;
1311 }
1312 ListMeasuringBehavior::Measure(has_measured) => {
1313 if *has_measured {
1314 return;
1315 }
1316 *has_measured = true;
1317 }
1318 }
1319
1320 let mut cursor = self.items.cursor::<Count>(());
1321 let available_item_space = size(
1322 AvailableSpace::Definite(available_width),
1323 AvailableSpace::MinContent,
1324 );
1325
1326 let mut measured_items = Vec::default();
1327
1328 for (ix, item) in cursor.enumerate() {
1329 let size = item.size().unwrap_or_else(|| {
1330 let mut element = render_item(ix, window, cx);
1331 element.layout_as_root(available_item_space, window, cx)
1332 });
1333
1334 measured_items.push(ListItem::Measured {
1335 size,
1336 focus_handle: item.focus_handle(),
1337 });
1338 }
1339
1340 self.items = SumTree::from_iter(measured_items, ());
1341 }
1342
1343 fn layout_items(
1344 &mut self,
1345 available_width: Option<Pixels>,
1346 available_height: Pixels,
1347 padding: &Edges<Pixels>,
1348 render_item: &mut RenderItemFn,
1349 window: &mut Window,
1350 cx: &mut App,
1351 ) -> LayoutItemsResponse {
1352 let old_items = self.items.clone();
1353 let mut measured_items = VecDeque::new();
1354 let mut item_layouts = VecDeque::new();
1355 let mut rendered_height = padding.top;
1356 let mut max_item_width = px(0.);
1357 let mut scroll_top = self.logical_scroll_top();
1358
1359 if self.follow_state.is_following() {
1360 scroll_top = ListOffset {
1361 item_ix: self.items.summary().count,
1362 offset_in_item: px(0.),
1363 };
1364 self.logical_scroll_top = Some(scroll_top);
1365 }
1366
1367 let mut rendered_focused_item = false;
1368
1369 let available_item_space = size(
1370 available_width.map_or(AvailableSpace::MaxContent, |width| {
1371 AvailableSpace::Definite(width)
1372 }),
1373 AvailableSpace::MinContent,
1374 );
1375
1376 let mut cursor = old_items.cursor::<Count>(());
1377
1378 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
1380 for (ix, item) in cursor.by_ref().enumerate() {
1381 let visible_height = rendered_height - scroll_top.offset_in_item;
1382 if visible_height >= available_height + self.overdraw {
1383 break;
1384 }
1385
1386 let mut size = item.size();
1388
1389 if visible_height < available_height || size.is_none() {
1391 let item_index = scroll_top.item_ix + ix;
1392 let mut element = render_item(item_index, window, cx);
1393 let element_size = element.layout_as_root(available_item_space, window, cx);
1394 size = Some(element_size);
1395
1396 if ix == 0 {
1399 if let Some(pending_scroll) = self.pending_scroll.take() {
1400 match pending_scroll {
1401 PendingScroll::Absolute { item_ix, offset }
1402 if item_ix == scroll_top.item_ix =>
1403 {
1404 scroll_top.offset_in_item = offset.min(element_size.height);
1405 self.logical_scroll_top = Some(scroll_top);
1406 }
1407 PendingScroll::Proportional(pending_scroll)
1408 if pending_scroll.item_ix == scroll_top.item_ix =>
1409 {
1410 scroll_top.offset_in_item =
1413 Pixels(pending_scroll.fraction * element_size.height.0);
1414 self.logical_scroll_top = Some(scroll_top);
1415 }
1416 _ => {}
1417 }
1418 }
1419 }
1420
1421 if visible_height < available_height {
1422 item_layouts.push_back(ItemLayout {
1423 index: item_index,
1424 element,
1425 size: element_size,
1426 });
1427 if item.contains_focused(window, cx) {
1428 rendered_focused_item = true;
1429 }
1430 }
1431 }
1432
1433 let size = size.unwrap();
1434 rendered_height += size.height;
1435 max_item_width = max_item_width.max(size.width);
1436 measured_items.push_back(ListItem::Measured {
1437 size,
1438 focus_handle: item.focus_handle(),
1439 });
1440 }
1441 rendered_height += padding.bottom;
1442
1443 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
1445
1446 if rendered_height - scroll_top.offset_in_item < available_height {
1449 while rendered_height < available_height {
1450 cursor.prev();
1451 if let Some(item) = cursor.item() {
1452 let item_index = cursor.start().0;
1453 let mut element = render_item(item_index, window, cx);
1454 let element_size = element.layout_as_root(available_item_space, window, cx);
1455 let focus_handle = item.focus_handle();
1456 rendered_height += element_size.height;
1457 measured_items.push_front(ListItem::Measured {
1458 size: element_size,
1459 focus_handle,
1460 });
1461 item_layouts.push_front(ItemLayout {
1462 index: item_index,
1463 element,
1464 size: element_size,
1465 });
1466 if item.contains_focused(window, cx) {
1467 rendered_focused_item = true;
1468 }
1469 } else {
1470 break;
1471 }
1472 }
1473
1474 scroll_top = ListOffset {
1475 item_ix: cursor.start().0,
1476 offset_in_item: rendered_height - available_height,
1477 };
1478
1479 match self.alignment {
1480 ListAlignment::Top => {
1481 scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
1482 self.logical_scroll_top = Some(scroll_top);
1483 }
1484 ListAlignment::Bottom => {
1485 scroll_top = ListOffset {
1486 item_ix: cursor.start().0,
1487 offset_in_item: rendered_height - available_height,
1488 };
1489 self.logical_scroll_top = None;
1490 }
1491 };
1492 }
1493
1494 let mut leading_overdraw = scroll_top.offset_in_item;
1496 while leading_overdraw < self.overdraw {
1497 cursor.prev();
1498 if let Some(item) = cursor.item() {
1499 let size = if let ListItem::Measured { size, .. } = item {
1500 *size
1501 } else {
1502 let mut element = render_item(cursor.start().0, window, cx);
1503 element.layout_as_root(available_item_space, window, cx)
1504 };
1505
1506 leading_overdraw += size.height;
1507 measured_items.push_front(ListItem::Measured {
1508 size,
1509 focus_handle: item.focus_handle(),
1510 });
1511 } else {
1512 break;
1513 }
1514 }
1515
1516 let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
1517 let mut cursor = old_items.cursor::<Count>(());
1518 let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right);
1519 new_items.extend(measured_items, ());
1520 cursor.seek(&Count(measured_range.end), Bias::Right);
1521 new_items.append(cursor.suffix(), ());
1522 self.items = new_items;
1523
1524 if self.follow_state.has_stopped_following() {
1528 let padding = self.last_padding.unwrap_or_default();
1529 let total_height = self.items.summary().height + padding.top + padding.bottom;
1530 let scroll_offset = self.scroll_top(&scroll_top);
1531 if scroll_offset + available_height >= total_height - px(1.0) {
1532 self.follow_state.start_following();
1533 }
1534 }
1535
1536 if !rendered_focused_item {
1540 let mut cursor = self
1541 .items
1542 .filter::<_, Count>((), |summary| summary.has_focus_handles);
1543 cursor.next();
1544 while let Some(item) = cursor.item() {
1545 if item.contains_focused(window, cx) {
1546 let item_index = cursor.start().0;
1547 let mut element = render_item(cursor.start().0, window, cx);
1548 let size = element.layout_as_root(available_item_space, window, cx);
1549 item_layouts.push_back(ItemLayout {
1550 index: item_index,
1551 element,
1552 size,
1553 });
1554 break;
1555 }
1556 cursor.next();
1557 }
1558 }
1559
1560 LayoutItemsResponse {
1561 max_item_width,
1562 scroll_top,
1563 item_layouts,
1564 }
1565 }
1566
1567 fn prepaint_items(
1568 &mut self,
1569 bounds: Bounds<Pixels>,
1570 padding: Edges<Pixels>,
1571 content_mask: ContentMask<Pixels>,
1572 autoscroll: bool,
1573 render_item: &mut RenderItemFn,
1574 window: &mut Window,
1575 cx: &mut App,
1576 ) -> Result<LayoutItemsResponse, ListOffset> {
1577 window.transact(|window| {
1578 match self.measuring_behavior {
1579 ListMeasuringBehavior::Measure(has_measured) if !has_measured => {
1580 self.layout_all_items(bounds.size.width, render_item, window, cx);
1581 }
1582 _ => {}
1583 }
1584
1585 let mut layout_response = self.layout_items(
1586 Some(bounds.size.width),
1587 bounds.size.height,
1588 &padding,
1589 render_item,
1590 window,
1591 cx,
1592 );
1593
1594 window.take_autoscroll();
1596
1597 if bounds.size.height > padding.top + padding.bottom {
1599 let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
1600 item_origin.y -= layout_response.scroll_top.offset_in_item;
1601 for item in &mut layout_response.item_layouts {
1602 window.with_content_mask(Some(content_mask), |window| {
1603 item.element.prepaint_at(item_origin, window, cx);
1604 });
1605
1606 if let Some(autoscroll_bounds) = window.take_autoscroll()
1607 && autoscroll
1608 {
1609 if autoscroll_bounds.top() < bounds.top() {
1610 let mut item_ix = item.index;
1611 let mut offset_in_item = autoscroll_bounds.top() - item_origin.y;
1612
1613 if offset_in_item < Pixels::ZERO {
1618 let mut cursor = self.items.cursor::<Count>(());
1619 cursor.seek(&Count(item_ix), Bias::Right);
1620 while offset_in_item < Pixels::ZERO {
1621 cursor.prev();
1622 let Some(prev_item) = cursor.item() else {
1623 offset_in_item = Pixels::ZERO;
1624 break;
1625 };
1626 let size = prev_item.size().unwrap_or_else(|| {
1627 let mut element = render_item(cursor.start().0, window, cx);
1628 let item_available_size = size(
1629 bounds.size.width.into(),
1630 AvailableSpace::MinContent,
1631 );
1632 element.layout_as_root(item_available_size, window, cx)
1633 });
1634 item_ix = cursor.start().0;
1635 offset_in_item += size.height;
1636 }
1637 }
1638
1639 return Err(ListOffset {
1640 item_ix,
1641 offset_in_item,
1642 });
1643 } else if autoscroll_bounds.bottom() > bounds.bottom() {
1644 let mut cursor = self.items.cursor::<Count>(());
1645 cursor.seek(&Count(item.index), Bias::Right);
1646 let mut height = bounds.size.height - padding.top - padding.bottom;
1647
1648 height -= autoscroll_bounds.bottom() - item_origin.y;
1650
1651 while height > Pixels::ZERO {
1653 cursor.prev();
1654 let Some(item) = cursor.item() else { break };
1655
1656 let size = item.size().unwrap_or_else(|| {
1657 let mut item = render_item(cursor.start().0, window, cx);
1658 let item_available_size =
1659 size(bounds.size.width.into(), AvailableSpace::MinContent);
1660 item.layout_as_root(item_available_size, window, cx)
1661 });
1662 height -= size.height;
1663 }
1664
1665 return Err(ListOffset {
1666 item_ix: cursor.start().0,
1667 offset_in_item: if height < Pixels::ZERO {
1668 -height
1669 } else {
1670 Pixels::ZERO
1671 },
1672 });
1673 }
1674 }
1675
1676 item_origin.y += item.size.height;
1677 }
1678 } else {
1679 layout_response.item_layouts.clear();
1680 }
1681
1682 Ok(layout_response)
1683 })
1684 }
1685
1686 fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
1689 let Some(bounds) = self.last_layout_bounds else {
1690 return;
1691 };
1692 let height = bounds.size.height;
1693
1694 let padding = self.last_padding.unwrap_or_default();
1695 let content_height = self
1698 .scrollbar_drag_start_height
1699 .unwrap_or_else(|| self.items.summary().height);
1700 let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
1701 let new_scroll_top = (-point.y).max(px(0.)).min(scroll_max);
1702
1703 let dragged_to_end =
1706 scroll_max > px(0.) && new_scroll_top >= (scroll_max - px(1.0)).max(px(0.));
1707 if dragged_to_end && matches!(self.follow_state, FollowState::Tail { .. }) {
1708 self.follow_state = FollowState::Tail { is_following: true };
1709 let item_count = self.items.summary().count;
1710 self.pending_scroll = None;
1711 self.logical_scroll_top = Some(ListOffset {
1712 item_ix: item_count,
1713 offset_in_item: px(0.),
1714 });
1715 return;
1716 }
1717
1718 self.follow_state.stop_following();
1719
1720 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
1721 self.pending_scroll = None;
1722 self.logical_scroll_top = None;
1723 } else {
1724 let (start, _, _) =
1725 self.items
1726 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
1727
1728 let scroll_top = ListOffset {
1729 item_ix: start.count,
1730 offset_in_item: new_scroll_top - start.height,
1731 };
1732 self.rebase_pending_scroll(scroll_top);
1733 self.logical_scroll_top = Some(scroll_top);
1734 }
1735 }
1736}
1737
1738impl std::fmt::Debug for ListItem {
1739 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1740 match self {
1741 Self::Unmeasured { .. } => write!(f, "Unrendered"),
1742 Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
1743 }
1744 }
1745}
1746
1747#[derive(Debug, Clone, Copy, Default)]
1751pub struct ListOffset {
1752 pub item_ix: usize,
1754 pub offset_in_item: Pixels,
1756}
1757
1758impl List {
1759 fn base_style(&self) -> Style {
1762 let mut style = Style::default();
1763 style.overflow.y = Overflow::Scroll;
1764 style.refine(&self.style);
1765 style
1766 }
1767}
1768
1769impl Element for List {
1770 type RequestLayoutState = ();
1771 type PrepaintState = ListPrepaintState;
1772
1773 fn id(&self) -> Option<crate::ElementId> {
1774 None
1775 }
1776
1777 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1778 None
1779 }
1780
1781 fn request_layout(
1782 &mut self,
1783 _id: Option<&GlobalElementId>,
1784 _inspector_id: Option<&InspectorElementId>,
1785 window: &mut Window,
1786 cx: &mut App,
1787 ) -> (crate::LayoutId, Self::RequestLayoutState) {
1788 cx.note_revision(&self.state.0.borrow().revision);
1789 let layout_id = match self.sizing_behavior {
1790 ListSizingBehavior::Infer => {
1791 let style = self.base_style();
1792 window.with_text_style(style.text_style().cloned(), |window| {
1793 let state = &mut *self.state.state_mut();
1794
1795 let available_height = if let Some(last_bounds) = state.last_layout_bounds {
1796 last_bounds.size.height
1797 } else {
1798 state.overdraw
1801 };
1802 let padding = style.padding.to_pixels(
1803 state.last_layout_bounds.unwrap_or_default().size.into(),
1804 window.rem_size(),
1805 );
1806
1807 let layout_response = state.layout_items(
1808 None,
1809 available_height,
1810 &padding,
1811 &mut self.render_item,
1812 window,
1813 cx,
1814 );
1815 let max_element_width = layout_response.max_item_width;
1816
1817 let summary = state.items.summary();
1818 let total_height = summary.height;
1819
1820 window.request_measured_layout(
1821 style,
1822 move |known_dimensions, available_space, _window, _cx| {
1823 let width =
1824 known_dimensions
1825 .width
1826 .unwrap_or(match available_space.width {
1827 AvailableSpace::Definite(x) => x,
1828 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1829 max_element_width
1830 }
1831 });
1832 let height = match available_space.height {
1833 AvailableSpace::Definite(height) => total_height.min(height),
1834 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1835 total_height
1836 }
1837 };
1838 size(width, height)
1839 },
1840 )
1841 })
1842 }
1843 ListSizingBehavior::Auto => {
1844 let mut style = Style::default();
1845 style.refine(&self.style);
1846 window.with_text_style(style.text_style().cloned(), |window| {
1847 window.request_layout(style, None, cx)
1848 })
1849 }
1850 };
1851 (layout_id, ())
1852 }
1853
1854 fn prepaint(
1855 &mut self,
1856 _id: Option<&GlobalElementId>,
1857 _inspector_id: Option<&InspectorElementId>,
1858 bounds: Bounds<Pixels>,
1859 _: &mut Self::RequestLayoutState,
1860 window: &mut Window,
1861 cx: &mut App,
1862 ) -> ListPrepaintState {
1863 let state = &mut *self.state.state_mut();
1864 state.reset = false;
1865
1866 let style = self.base_style();
1867 let content_mask = style.scroll_mask(bounds, window.rem_size());
1868
1869 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1870
1871 if state
1875 .last_layout_bounds
1876 .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width)
1877 {
1878 let new_items = SumTree::from_iter(
1879 state.items.iter().map(|item| ListItem::Unmeasured {
1880 size_hint: match item {
1881 ListItem::Unmeasured { size_hint, .. } => *size_hint,
1882 ListItem::Measured { .. } => None,
1883 },
1884 focus_handle: item.focus_handle(),
1885 }),
1886 (),
1887 );
1888
1889 state.items = new_items;
1890 state.measuring_behavior.reset();
1891 }
1892
1893 let padding = style
1894 .padding
1895 .to_pixels(bounds.size.into(), window.rem_size());
1896 let layout = match state.prepaint_items(
1897 bounds,
1898 padding,
1899 content_mask,
1900 true,
1901 &mut self.render_item,
1902 window,
1903 cx,
1904 ) {
1905 Ok(layout) => layout,
1906 Err(autoscroll_request) => {
1907 state.logical_scroll_top = Some(autoscroll_request);
1908 state
1909 .prepaint_items(
1910 bounds,
1911 padding,
1912 content_mask,
1913 false,
1914 &mut self.render_item,
1915 window,
1916 cx,
1917 )
1918 .unwrap()
1919 }
1920 };
1921
1922 state.last_layout_bounds = Some(bounds);
1923 state.last_padding = Some(padding);
1924 ListPrepaintState { hitbox, layout }
1925 }
1926
1927 fn paint(
1928 &mut self,
1929 _id: Option<&GlobalElementId>,
1930 _inspector_id: Option<&InspectorElementId>,
1931 bounds: Bounds<crate::Pixels>,
1932 _: &mut Self::RequestLayoutState,
1933 prepaint: &mut Self::PrepaintState,
1934 window: &mut Window,
1935 cx: &mut App,
1936 ) {
1937 let current_view = window.current_view();
1938
1939 let list_state = self.state.clone();
1945 let height = bounds.size.height;
1946 let scroll_top = prepaint.layout.scroll_top;
1947 let hitbox_id = prepaint.hitbox.id;
1948 let mut accumulated_scroll_delta = ScrollDelta::default();
1949 window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
1950 if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) {
1951 accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
1952 let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
1953 list_state.state_mut().scroll(
1954 &scroll_top,
1955 height,
1956 pixel_delta,
1957 current_view,
1958 window,
1959 cx,
1960 )
1961 }
1962 });
1963
1964 let style = self.base_style();
1965 window.with_content_mask(
1966 Some(style.scroll_mask(bounds, window.rem_size())),
1967 |window| {
1968 for item in &mut prepaint.layout.item_layouts {
1969 item.element.paint(window, cx);
1970 }
1971 },
1972 );
1973 }
1974}
1975
1976impl IntoElement for List {
1977 type Element = Self;
1978
1979 fn into_element(self) -> Self::Element {
1980 self
1981 }
1982}
1983
1984impl Styled for List {
1985 fn style(&mut self) -> &mut StyleRefinement {
1986 &mut self.style
1987 }
1988}
1989
1990impl sum_tree::Item for ListItem {
1991 type Summary = ListItemSummary;
1992
1993 fn summary(&self, _: ()) -> Self::Summary {
1994 match self {
1995 ListItem::Unmeasured {
1996 size_hint,
1997 focus_handle,
1998 } => ListItemSummary {
1999 count: 1,
2000 rendered_count: 0,
2001 unrendered_count: 1,
2002 height: if let Some(size) = size_hint {
2003 size.height
2004 } else {
2005 px(0.)
2006 },
2007 has_focus_handles: focus_handle.is_some(),
2008 has_unknown_height: size_hint.is_none(),
2009 },
2010 ListItem::Measured {
2011 size, focus_handle, ..
2012 } => ListItemSummary {
2013 count: 1,
2014 rendered_count: 1,
2015 unrendered_count: 0,
2016 height: size.height,
2017 has_focus_handles: focus_handle.is_some(),
2018 has_unknown_height: false,
2019 },
2020 }
2021 }
2022}
2023
2024impl sum_tree::ContextLessSummary for ListItemSummary {
2025 fn zero() -> Self {
2026 Default::default()
2027 }
2028
2029 fn add_summary(&mut self, summary: &Self) {
2030 self.count += summary.count;
2031 self.rendered_count += summary.rendered_count;
2032 self.unrendered_count += summary.unrendered_count;
2033 self.height += summary.height;
2034 self.has_focus_handles |= summary.has_focus_handles;
2035 self.has_unknown_height |= summary.has_unknown_height;
2036 }
2037}
2038
2039impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
2040 fn zero(_cx: ()) -> Self {
2041 Default::default()
2042 }
2043
2044 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
2045 self.0 += summary.count;
2046 }
2047}
2048
2049impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
2050 fn zero(_cx: ()) -> Self {
2051 Default::default()
2052 }
2053
2054 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
2055 self.0 += summary.height;
2056 }
2057}
2058
2059impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Count {
2060 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
2061 self.0.partial_cmp(&other.count).unwrap()
2062 }
2063}
2064
2065impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height {
2066 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
2067 self.0.partial_cmp(&other.height).unwrap()
2068 }
2069}
2070
2071#[cfg(test)]
2072mod test {
2073
2074 use gpui::{ScrollDelta, ScrollWheelEvent};
2075 use std::cell::{Cell, RefCell};
2076 use std::rc::Rc;
2077
2078 use crate::{
2079 self as gpui, AppContext, Bounds, Context, Element, FollowMode, InteractiveElement,
2080 IntoElement, ListState, Render, Styled, TestAppContext, Window, canvas, div, list, point,
2081 px, size,
2082 };
2083
2084 #[gpui::test]
2085 fn test_autoscroll_above_item_top_renders_items_above(cx: &mut TestAppContext) {
2086 let cx = cx.add_empty_window();
2087
2088 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
2089 state.scroll_to(gpui::ListOffset {
2090 item_ix: 2,
2091 offset_in_item: px(0.),
2092 });
2093
2094 struct TestView(ListState);
2095 impl Render for TestView {
2096 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2097 list(self.0.clone(), |ix, _, _| {
2098 if ix == 2 {
2099 canvas(
2102 |bounds, window, _| {
2103 window.request_autoscroll(Bounds::from_corners(
2104 point(bounds.left(), bounds.top() - px(30.)),
2105 point(bounds.right(), bounds.top() + px(5.)),
2106 ));
2107 },
2108 |_, _, _, _| {},
2109 )
2110 .h(px(20.))
2111 .w_full()
2112 .into_any()
2113 } else {
2114 div().h(px(20.)).w_full().into_any()
2115 }
2116 })
2117 .w_full()
2118 .h_full()
2119 }
2120 }
2121
2122 cx.draw(point(px(0.), px(0.)), size(px(100.), px(60.)), |_, cx| {
2123 cx.new(|_| TestView(state.clone())).into_any_element()
2124 });
2125
2126 let scroll_top = state.logical_scroll_top();
2128 assert!(
2129 scroll_top.offset_in_item >= px(0.),
2130 "offset_in_item must never be negative (would leave blank space above), got {:?}",
2131 scroll_top.offset_in_item,
2132 );
2133 assert_eq!(scroll_top.item_ix, 0);
2134 assert_eq!(scroll_top.offset_in_item, px(10.));
2135 }
2136
2137 #[gpui::test]
2138 fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
2139 let cx = cx.add_empty_window();
2140
2141 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
2142
2143 state.scroll_to(gpui::ListOffset {
2145 item_ix: 0,
2146 offset_in_item: px(0.0),
2147 });
2148
2149 struct TestView(ListState);
2150 impl Render for TestView {
2151 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2152 list(self.0.clone(), |_, _, _| {
2153 div().h(px(10.)).w_full().into_any()
2154 })
2155 .w_full()
2156 .h_full()
2157 }
2158 }
2159
2160 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2162 cx.new(|_| TestView(state.clone())).into_any_element()
2163 });
2164
2165 state.reset(5);
2167
2168 cx.simulate_event(ScrollWheelEvent {
2170 position: point(px(1.), px(1.)),
2171 delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
2172 ..Default::default()
2173 });
2174
2175 assert_eq!(state.logical_scroll_top().item_ix, 0);
2177 assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
2178 }
2179
2180 #[gpui::test]
2181 fn test_uniform_item_height_survives_the_first_layout(cx: &mut TestAppContext) {
2182 let cx = cx.add_empty_window();
2183
2184 let state =
2185 ListState::new(10, crate::ListAlignment::Top, px(0.)).with_uniform_item_height(px(50.));
2186
2187 struct TestView(ListState);
2188 impl Render for TestView {
2189 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2190 list(self.0.clone(), |_, _, _| {
2191 div().h(px(50.)).w_full().into_any()
2192 })
2193 .w_full()
2194 .h_full()
2195 }
2196 }
2197
2198 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, cx| {
2200 cx.new(|_| TestView(state.clone())).into_any_element()
2201 });
2202 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
2203
2204 state.scroll_to_reveal_item(9);
2206 let top = state.logical_scroll_top();
2207 assert_eq!(px(50. * top.item_ix as f32) + top.offset_in_item, px(300.));
2208 }
2209
2210 #[gpui::test]
2211 fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) {
2212 let cx = cx.add_empty_window();
2213
2214 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
2215
2216 struct TestView(ListState);
2217 impl Render for TestView {
2218 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2219 list(self.0.clone(), |_, _, _| {
2220 div().h(px(20.)).w_full().into_any()
2221 })
2222 .w_full()
2223 .h_full()
2224 }
2225 }
2226
2227 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
2229 cx.new(|_| TestView(state.clone())).into_any_element()
2230 });
2231
2232 state.scroll_by(px(30.));
2234
2235 let offset = state.logical_scroll_top();
2237 assert_eq!(offset.item_ix, 1);
2238 assert_eq!(offset.offset_in_item, px(10.));
2239
2240 state.scroll_by(px(-30.));
2242
2243 let offset = state.logical_scroll_top();
2245 assert_eq!(offset.item_ix, 0);
2246 assert_eq!(offset.offset_in_item, px(0.));
2247
2248 state.scroll_by(px(0.));
2250 let offset = state.logical_scroll_top();
2251 assert_eq!(offset.item_ix, 0);
2252 assert_eq!(offset.offset_in_item, px(0.));
2253 }
2254
2255 #[gpui::test]
2256 fn test_scroll_handler_reports_the_resulting_visible_range(cx: &mut TestAppContext) {
2257 struct TestView(ListState);
2258 impl Render for TestView {
2259 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2260 list(self.0.clone(), |_, _, _| {
2261 div().h(px(20.)).w_full().into_any()
2262 })
2263 .w_full()
2264 .h_full()
2265 }
2266 }
2267
2268 for alignment in [crate::ListAlignment::Top, crate::ListAlignment::Bottom] {
2269 let cx = cx.add_empty_window();
2270 let state = ListState::new(5, alignment, px(0.)).measure_all();
2271 state.scroll_to(gpui::ListOffset {
2272 item_ix: 0,
2273 offset_in_item: px(0.),
2274 });
2275 let reported = Rc::new(RefCell::new(Vec::new()));
2276 state.set_scroll_handler({
2277 let reported = reported.clone();
2278 move |event, _, _| {
2279 reported.borrow_mut().push((
2280 event.visible_range.clone(),
2281 event.count,
2282 event.is_scrolled,
2283 ));
2284 }
2285 });
2286 cx.draw(point(px(0.), px(0.)), size(px(100.), px(40.)), |_, cx| {
2287 cx.new(|_| TestView(state.clone())).into_any_element()
2288 });
2289
2290 for distance in [-40., -1000.] {
2292 cx.simulate_event(ScrollWheelEvent {
2293 position: point(px(50.), px(10.)),
2294 delta: ScrollDelta::Pixels(point(px(0.), px(distance))),
2295 ..Default::default()
2296 });
2297 }
2298
2299 assert_eq!(
2300 &*reported.borrow(),
2301 &[
2302 (2..4, 5, true),
2303 (3..5, 5, alignment == crate::ListAlignment::Top),
2304 ],
2305 "{alignment:?}: callbacks should report the viewport after the scroll"
2306 );
2307 }
2308 }
2309
2310 #[gpui::test]
2311 fn test_child_scroll_handler_can_stop_list_scroll(cx: &mut TestAppContext) {
2312 let cx = cx.add_empty_window();
2313
2314 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
2315 let child_saw_event = Rc::new(Cell::new(false));
2316
2317 struct TestView {
2318 state: ListState,
2319 child_saw_event: Rc<Cell<bool>>,
2320 }
2321 impl Render for TestView {
2322 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2323 let child_saw_event = self.child_saw_event.clone();
2324 list(self.state.clone(), move |_, _, _| {
2325 let child_saw_event = child_saw_event.clone();
2326 div()
2327 .h(px(20.))
2328 .w_full()
2329 .on_scroll_wheel(move |_, _, cx| {
2330 child_saw_event.set(true);
2331 cx.stop_propagation();
2332 })
2333 .into_any()
2334 })
2335 .w_full()
2336 .h_full()
2337 }
2338 }
2339
2340 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
2341 cx.new(|_| TestView {
2342 state: state.clone(),
2343 child_saw_event: child_saw_event.clone(),
2344 })
2345 .into_any_element()
2346 });
2347
2348 cx.simulate_event(ScrollWheelEvent {
2349 position: point(px(50.), px(10.)),
2350 delta: ScrollDelta::Pixels(point(px(0.), px(-30.))),
2351 ..Default::default()
2352 });
2353
2354 assert!(
2355 child_saw_event.get(),
2356 "the child's scroll-wheel handler should run"
2357 );
2358 let offset = state.logical_scroll_top();
2360 assert_eq!(offset.item_ix, 0);
2361 assert_eq!(offset.offset_in_item, px(0.));
2362 }
2363
2364 struct TestListView(ListState);
2365 impl Render for TestListView {
2366 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2367 list(self.0.clone(), |_, _, _| {
2368 div().h(px(20.)).w_full().into_any()
2369 })
2370 .w_full()
2371 .h_full()
2372 }
2373 }
2374
2375 #[gpui::test]
2376 fn test_item_viewport_queries_return_none_before_layout(_cx: &mut TestAppContext) {
2377 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2378
2379 assert_eq!(state.item_is_above_viewport(0), None);
2380 assert_eq!(state.item_is_below_viewport(0), None);
2381 }
2382
2383 #[gpui::test]
2384 fn test_item_viewport_queries_before_logical_scroll_top(cx: &mut TestAppContext) {
2385 let cx = cx.add_empty_window();
2386
2387 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2388
2389 state.scroll_to(gpui::ListOffset {
2390 item_ix: 2,
2391 offset_in_item: px(0.),
2392 });
2393 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2394 cx.new(|_| TestListView(state.clone())).into_any_element()
2395 });
2396
2397 assert_eq!(state.item_is_above_viewport(1), Some(true));
2398 assert_eq!(state.item_is_below_viewport(1), Some(false));
2399 }
2400
2401 #[gpui::test]
2402 fn test_item_viewport_queries_measured_item_inside_viewport(cx: &mut TestAppContext) {
2403 let cx = cx.add_empty_window();
2404
2405 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2406
2407 state.scroll_to(gpui::ListOffset {
2408 item_ix: 2,
2409 offset_in_item: px(0.),
2410 });
2411 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2412 cx.new(|_| TestListView(state.clone())).into_any_element()
2413 });
2414
2415 assert_eq!(state.item_is_above_viewport(2), Some(false));
2416 assert_eq!(state.item_is_below_viewport(2), Some(false));
2417 }
2418
2419 #[gpui::test]
2420 fn test_item_viewport_queries_measured_item_above_viewport(cx: &mut TestAppContext) {
2421 let cx = cx.add_empty_window();
2422
2423 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2424
2425 state.scroll_to(gpui::ListOffset {
2426 item_ix: 2,
2427 offset_in_item: px(20.),
2428 });
2429 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2430 cx.new(|_| TestListView(state.clone())).into_any_element()
2431 });
2432
2433 assert_eq!(state.item_is_above_viewport(2), Some(true));
2434 assert_eq!(state.item_is_below_viewport(2), Some(false));
2435 }
2436
2437 #[gpui::test]
2438 fn test_item_viewport_queries_measured_item_below_viewport(cx: &mut TestAppContext) {
2439 let cx = cx.add_empty_window();
2440
2441 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2442
2443 state.scroll_to(gpui::ListOffset {
2444 item_ix: 2,
2445 offset_in_item: px(0.),
2446 });
2447 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2448 cx.new(|_| TestListView(state.clone())).into_any_element()
2449 });
2450
2451 assert_eq!(state.item_is_above_viewport(3), Some(false));
2452 assert_eq!(state.item_is_below_viewport(3), Some(true));
2453 }
2454
2455 #[gpui::test]
2456 fn test_item_viewport_queries_remain_stable_with_zero_height_viewport(cx: &mut TestAppContext) {
2457 let cx = cx.add_empty_window();
2458
2459 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2460
2461 state.scroll_to(gpui::ListOffset {
2462 item_ix: 2,
2463 offset_in_item: px(0.),
2464 });
2465 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2466 cx.new(|_| TestListView(state.clone())).into_any_element()
2467 });
2468
2469 assert_eq!(state.item_is_above_viewport(3), Some(false));
2470 assert_eq!(state.item_is_below_viewport(3), Some(true));
2471
2472 cx.draw(point(px(0.), px(0.)), size(px(100.), px(0.)), |_, cx| {
2477 cx.new(|_| TestListView(state.clone())).into_any_element()
2478 });
2479
2480 assert_eq!(state.item_is_above_viewport(1), Some(true));
2481 assert_eq!(state.item_is_below_viewport(1), Some(false));
2482 assert_eq!(state.item_is_above_viewport(3), Some(false));
2483 assert_eq!(state.item_is_below_viewport(3), Some(true));
2484 }
2485
2486 #[gpui::test]
2487 fn test_item_viewport_queries_after_scroll_to_end_before_layout(cx: &mut TestAppContext) {
2488 let cx = cx.add_empty_window();
2489
2490 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2491
2492 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2493 cx.new(|_| TestListView(state.clone())).into_any_element()
2494 });
2495
2496 state.scroll_to_end();
2497
2498 assert_eq!(state.logical_scroll_top().item_ix, state.item_count());
2499 assert_eq!(state.item_is_above_viewport(0), Some(true));
2500 assert_eq!(state.item_is_below_viewport(0), Some(false));
2501 }
2502
2503 #[gpui::test]
2504 fn test_measure_all_after_width_change(cx: &mut TestAppContext) {
2505 let cx = cx.add_empty_window();
2506
2507 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
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 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2525 view.clone().into_any_element()
2526 });
2527 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
2528
2529 cx.draw(point(px(0.), px(0.)), size(px(200.), px(200.)), |_, _| {
2533 view.into_any_element()
2534 });
2535 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
2536 }
2537
2538 #[gpui::test]
2539 fn test_remeasure(cx: &mut TestAppContext) {
2540 let cx = cx.add_empty_window();
2541
2542 let item_height = Rc::new(Cell::new(100usize));
2546 let state = ListState::new(10, crate::ListAlignment::Top, px(10.));
2547
2548 struct TestView {
2549 state: ListState,
2550 item_height: Rc<Cell<usize>>,
2551 }
2552
2553 impl Render for TestView {
2554 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2555 let height = self.item_height.get();
2556 list(self.state.clone(), move |_, _, _| {
2557 div().h(px(height as f32)).w_full().into_any()
2558 })
2559 .w_full()
2560 .h_full()
2561 }
2562 }
2563
2564 let state_clone = state.clone();
2565 let item_height_clone = item_height.clone();
2566 let view = cx.update(|_, cx| {
2567 cx.new(|_| TestView {
2568 state: state_clone,
2569 item_height: item_height_clone,
2570 })
2571 });
2572
2573 state.scroll_to(gpui::ListOffset {
2576 item_ix: 2,
2577 offset_in_item: px(40.),
2578 });
2579
2580 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2581 view.clone().into_any_element()
2582 });
2583
2584 let offset = state.logical_scroll_top();
2585 assert_eq!(offset.item_ix, 2);
2586 assert_eq!(offset.offset_in_item, px(40.));
2587
2588 item_height.set(50);
2593 state.remeasure();
2594
2595 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2596 view.into_any_element()
2597 });
2598
2599 let offset = state.logical_scroll_top();
2600 assert_eq!(offset.item_ix, 2);
2601 assert_eq!(offset.offset_in_item, px(20.));
2602 }
2603
2604 #[gpui::test]
2605 fn test_remeasure_item_preserves_scroll_offset(cx: &mut TestAppContext) {
2606 let cx = cx.add_empty_window();
2607
2608 let item_height = Rc::new(Cell::new(100usize));
2609 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2610
2611 struct TestView {
2612 state: ListState,
2613 item_height: Rc<Cell<usize>>,
2614 }
2615
2616 impl Render for TestView {
2617 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2618 let height = self.item_height.get();
2619 list(self.state.clone(), move |index, _, _| {
2620 let height = if index == 5 { height } else { 100 };
2621 div().h(px(height as f32)).w_full().into_any()
2622 })
2623 .w_full()
2624 .h_full()
2625 }
2626 }
2627
2628 let state_clone = state.clone();
2629 let item_height_clone = item_height.clone();
2630 let view = cx.update(|_, cx| {
2631 cx.new(|_| TestView {
2632 state: state_clone,
2633 item_height: item_height_clone,
2634 })
2635 });
2636
2637 state.scroll_to(gpui::ListOffset {
2638 item_ix: 5,
2639 offset_in_item: px(40.),
2640 });
2641
2642 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2643 view.clone().into_any_element()
2644 });
2645
2646 item_height.set(200);
2647 state.remeasure_items(5..6);
2648
2649 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2650 view.into_any_element()
2651 });
2652
2653 let offset = state.logical_scroll_top();
2654 assert_eq!(offset.item_ix, 5);
2655 assert_eq!(offset.offset_in_item, px(40.));
2656 }
2657
2658 #[gpui::test]
2659 fn test_remeasure_then_scroll_does_not_revert_scroll_position(cx: &mut TestAppContext) {
2660 let cx = cx.add_empty_window();
2661
2662 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2663
2664 struct TestView(ListState);
2665 impl Render for TestView {
2666 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2667 list(self.0.clone(), |_, _, _| {
2668 div().h(px(100.)).w_full().into_any()
2669 })
2670 .w_full()
2671 .h_full()
2672 }
2673 }
2674
2675 let view = {
2676 let state = state.clone();
2677 cx.update(|_, cx| cx.new(|_| TestView(state)))
2678 };
2679
2680 state.scroll_to(gpui::ListOffset {
2681 item_ix: 5,
2682 offset_in_item: px(40.),
2683 });
2684
2685 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2686 view.clone().into_any_element()
2687 });
2688
2689 state.remeasure_items(5..6);
2690
2691 cx.simulate_event(ScrollWheelEvent {
2692 position: point(px(50.), px(100.)),
2693 delta: ScrollDelta::Pixels(point(px(0.), px(-30.))),
2694 ..Default::default()
2695 });
2696
2697 let offset = state.logical_scroll_top();
2698 assert_eq!(offset.item_ix, 5);
2699 assert_eq!(offset.offset_in_item, px(70.));
2700
2701 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2702 view.into_any_element()
2703 });
2704
2705 let offset = state.logical_scroll_top();
2706 assert_eq!(offset.item_ix, 5);
2707 assert_eq!(
2708 offset.offset_in_item,
2709 px(70.),
2710 "scrolling after a remeasure should not be reverted by the stale pending scroll"
2711 );
2712 }
2713
2714 #[gpui::test]
2715 fn test_scroll_after_remeasure_clamps_to_shrunk_item_height(cx: &mut TestAppContext) {
2716 let cx = cx.add_empty_window();
2717
2718 let item_height = Rc::new(Cell::new(100usize));
2719 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2720
2721 struct TestView {
2722 state: ListState,
2723 item_height: Rc<Cell<usize>>,
2724 }
2725
2726 impl Render for TestView {
2727 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2728 let height = self.item_height.get();
2729 list(self.state.clone(), move |index, _, _| {
2730 let height = if index == 5 { height } else { 100 };
2731 div().h(px(height as f32)).w_full().into_any()
2732 })
2733 .w_full()
2734 .h_full()
2735 }
2736 }
2737
2738 let view = {
2739 let state = state.clone();
2740 let item_height = item_height.clone();
2741 cx.update(|_, cx| cx.new(|_| TestView { state, item_height }))
2742 };
2743
2744 state.scroll_to(gpui::ListOffset {
2745 item_ix: 5,
2746 offset_in_item: px(40.),
2747 });
2748
2749 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2750 view.clone().into_any_element()
2751 });
2752
2753 item_height.set(50);
2755 state.remeasure_items(5..6);
2756
2757 cx.simulate_event(ScrollWheelEvent {
2760 position: point(px(50.), px(100.)),
2761 delta: ScrollDelta::Pixels(point(px(0.), px(-30.))),
2762 ..Default::default()
2763 });
2764
2765 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2766 view.into_any_element()
2767 });
2768
2769 let offset = state.logical_scroll_top();
2772 assert_eq!(offset.item_ix, 5);
2773 assert_eq!(offset.offset_in_item, px(50.));
2774 }
2775
2776 #[gpui::test]
2777 fn test_follow_tail_stays_at_bottom_as_items_grow(cx: &mut TestAppContext) {
2778 let cx = cx.add_empty_window();
2779
2780 let item_height = Rc::new(Cell::new(50usize));
2783 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2784
2785 struct TestView {
2786 state: ListState,
2787 item_height: Rc<Cell<usize>>,
2788 }
2789 impl Render for TestView {
2790 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2791 let height = self.item_height.get();
2792 list(self.state.clone(), move |_, _, _| {
2793 div().h(px(height as f32)).w_full().into_any()
2794 })
2795 .w_full()
2796 .h_full()
2797 }
2798 }
2799
2800 let state_clone = state.clone();
2801 let item_height_clone = item_height.clone();
2802 let view = cx.update(|_, cx| {
2803 cx.new(|_| TestView {
2804 state: state_clone,
2805 item_height: item_height_clone,
2806 })
2807 });
2808
2809 state.set_follow_mode(FollowMode::Tail);
2810
2811 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2814 view.clone().into_any_element()
2815 });
2816
2817 let offset = state.logical_scroll_top();
2820 assert_eq!(offset.item_ix, 6);
2821 assert_eq!(offset.offset_in_item, px(0.));
2822 assert!(state.is_following_tail());
2823
2824 item_height.set(80);
2827 state.remeasure();
2828
2829 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2830 view.into_any_element()
2831 });
2832
2833 let offset = state.logical_scroll_top();
2840 assert_eq!(offset.item_ix, 7);
2841 assert_eq!(offset.offset_in_item, px(40.));
2842 assert!(state.is_following_tail());
2843 }
2844
2845 #[gpui::test]
2846 fn test_pause_following_tail_reengages_when_still_at_bottom(cx: &mut TestAppContext) {
2847 let cx = cx.add_empty_window();
2848
2849 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2851
2852 struct TestView(ListState);
2853 impl Render for TestView {
2854 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2855 list(self.0.clone(), |_, _, _| {
2856 div().h(px(50.)).w_full().into_any()
2857 })
2858 .w_full()
2859 .h_full()
2860 }
2861 }
2862
2863 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2864 state.set_follow_mode(FollowMode::Tail);
2865
2866 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2867 view.clone().into_any_element()
2868 });
2869 assert!(state.is_following_tail());
2870
2871 state.pause_following_tail();
2875 assert!(!state.is_following_tail());
2876
2877 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2878 view.into_any_element()
2879 });
2880 assert!(
2881 state.is_following_tail(),
2882 "pausing while at the bottom must re-engage follow-tail on the next layout"
2883 );
2884 }
2885
2886 #[gpui::test]
2887 fn test_pause_following_tail_freezes_off_bottom(cx: &mut TestAppContext) {
2888 let cx = cx.add_empty_window();
2889
2890 let item_height = Rc::new(Cell::new(50usize));
2893 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2894
2895 struct TestView {
2896 state: ListState,
2897 item_height: Rc<Cell<usize>>,
2898 }
2899 impl Render for TestView {
2900 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2901 let height = self.item_height.get();
2902 list(self.state.clone(), move |_, _, _| {
2903 div().h(px(height as f32)).w_full().into_any()
2904 })
2905 .w_full()
2906 .h_full()
2907 }
2908 }
2909
2910 let view = cx.update(|_, cx| {
2911 cx.new(|_| TestView {
2912 state: state.clone(),
2913 item_height: item_height.clone(),
2914 })
2915 });
2916 state.set_follow_mode(FollowMode::Tail);
2917
2918 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2920 view.clone().into_any_element()
2921 });
2922 assert_eq!(state.logical_scroll_top().item_ix, 6);
2923 assert!(state.is_following_tail());
2924
2925 state.pause_following_tail();
2929 item_height.set(80);
2930 state.remeasure();
2931 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2932 view.clone().into_any_element()
2933 });
2934 let offset = state.logical_scroll_top();
2935 assert_eq!(offset.item_ix, 6);
2936 assert_eq!(offset.offset_in_item, px(0.));
2937 assert!(
2938 !state.is_following_tail(),
2939 "a paused list must not re-engage while the frozen top is off the bottom"
2940 );
2941
2942 item_height.set(50);
2945 state.remeasure();
2946 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2947 view.into_any_element()
2948 });
2949 assert!(
2950 state.is_following_tail(),
2951 "returning to the bottom must restore follow-tail"
2952 );
2953 }
2954
2955 #[gpui::test]
2956 fn test_follow_tail_disengages_on_user_scroll(cx: &mut TestAppContext) {
2957 let cx = cx.add_empty_window();
2958
2959 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2961
2962 struct TestView(ListState);
2963 impl Render for TestView {
2964 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2965 list(self.0.clone(), |_, _, _| {
2966 div().h(px(50.)).w_full().into_any()
2967 })
2968 .w_full()
2969 .h_full()
2970 }
2971 }
2972
2973 state.set_follow_mode(FollowMode::Tail);
2974
2975 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, cx| {
2977 cx.new(|_| TestView(state.clone())).into_any_element()
2978 });
2979 assert!(state.is_following_tail());
2980
2981 cx.simulate_event(ScrollWheelEvent {
2984 position: point(px(50.), px(100.)),
2985 delta: ScrollDelta::Pixels(point(px(0.), px(100.))),
2986 ..Default::default()
2987 });
2988
2989 assert!(
2990 !state.is_following_tail(),
2991 "follow-tail should disengage when the user scrolls toward the start"
2992 );
2993 }
2994
2995 #[gpui::test]
2996 fn test_follow_tail_disengages_on_scrollbar_reposition(cx: &mut TestAppContext) {
2997 let cx = cx.add_empty_window();
2998
2999 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3001
3002 struct TestView(ListState);
3003 impl Render for TestView {
3004 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3005 list(self.0.clone(), |_, _, _| {
3006 div().h(px(50.)).w_full().into_any()
3007 })
3008 .w_full()
3009 .h_full()
3010 }
3011 }
3012
3013 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3014
3015 state.set_follow_mode(FollowMode::Tail);
3016
3017 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3019 view.clone().into_any_element()
3020 });
3021 assert!(state.is_following_tail());
3022
3023 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
3025
3026 let offset = state.logical_scroll_top();
3027 assert_eq!(offset.item_ix, 3);
3028 assert_eq!(offset.offset_in_item, px(0.));
3029 assert!(
3030 !state.is_following_tail(),
3031 "follow-tail should disengage when the scrollbar manually repositions the list"
3032 );
3033
3034 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3037 view.into_any_element()
3038 });
3039
3040 let offset = state.logical_scroll_top();
3041 assert_eq!(offset.item_ix, 3);
3042 assert_eq!(offset.offset_in_item, px(0.));
3043 }
3044
3045 #[gpui::test]
3046 fn test_scrollbar_drag_with_growing_content(cx: &mut TestAppContext) {
3047 let cx = cx.add_empty_window();
3048
3049 let last_item_height = Rc::new(Cell::new(50usize));
3050 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3051
3052 struct TestView {
3053 state: ListState,
3054 last_item_height: Rc<Cell<usize>>,
3055 }
3056 impl Render for TestView {
3057 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3058 let last_item_height = self.last_item_height.clone();
3059 list(self.state.clone(), move |index, _, _| {
3060 let height = if index == 9 {
3061 last_item_height.get()
3062 } else {
3063 50
3064 };
3065 div().h(px(height as f32)).w_full().into_any()
3066 })
3067 .w_full()
3068 .h_full()
3069 }
3070 }
3071
3072 let view = cx.update(|_, cx| {
3073 cx.new(|_| TestView {
3074 state: state.clone(),
3075 last_item_height: last_item_height.clone(),
3076 })
3077 });
3078
3079 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3080 view.clone().into_any_element()
3081 });
3082
3083 state.scrollbar_drag_started();
3084
3085 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
3086 let scrollbar_offset_before_growth = state.scroll_px_offset_for_scrollbar();
3087
3088 let offset = state.logical_scroll_top();
3089 assert_eq!(offset.item_ix, 3);
3090 assert_eq!(offset.offset_in_item, px(0.));
3091
3092 last_item_height.set(550);
3093 state.remeasure_items(9..10);
3094 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3095 view.clone().into_any_element()
3096 });
3097
3098 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
3099 assert_eq!(
3100 state.scroll_px_offset_for_scrollbar(),
3101 scrollbar_offset_before_growth
3102 );
3103
3104 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
3105 let offset = state.logical_scroll_top();
3106 assert_eq!(offset.item_ix, 3);
3107 assert_eq!(offset.offset_in_item, px(0.));
3108 }
3109
3110 #[gpui::test]
3111 fn test_set_follow_tail_snaps_to_bottom(cx: &mut TestAppContext) {
3112 let cx = cx.add_empty_window();
3113
3114 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
3116
3117 struct TestView(ListState);
3118 impl Render for TestView {
3119 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3120 list(self.0.clone(), |_, _, _| {
3121 div().h(px(50.)).w_full().into_any()
3122 })
3123 .w_full()
3124 .h_full()
3125 }
3126 }
3127
3128 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3129
3130 state.scroll_to(gpui::ListOffset {
3132 item_ix: 3,
3133 offset_in_item: px(0.),
3134 });
3135
3136 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3137 view.clone().into_any_element()
3138 });
3139
3140 let offset = state.logical_scroll_top();
3141 assert_eq!(offset.item_ix, 3);
3142 assert_eq!(offset.offset_in_item, px(0.));
3143 assert!(!state.is_following_tail());
3144
3145 state.set_follow_mode(FollowMode::Tail);
3148
3149 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3150 view.into_any_element()
3151 });
3152
3153 let offset = state.logical_scroll_top();
3156 assert_eq!(offset.item_ix, 6);
3157 assert_eq!(offset.offset_in_item, px(0.));
3158 assert!(state.is_following_tail());
3159 }
3160
3161 #[gpui::test]
3162 fn test_bottom_aligned_scrollbar_offset_at_end(cx: &mut TestAppContext) {
3163 let cx = cx.add_empty_window();
3164
3165 const ITEMS: usize = 10;
3166 const ITEM_SIZE: f32 = 50.0;
3167
3168 let state = ListState::new(
3169 ITEMS,
3170 crate::ListAlignment::Bottom,
3171 px(ITEMS as f32 * ITEM_SIZE),
3172 );
3173
3174 struct TestView(ListState);
3175 impl Render for TestView {
3176 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3177 list(self.0.clone(), |_, _, _| {
3178 div().h(px(ITEM_SIZE)).w_full().into_any()
3179 })
3180 .w_full()
3181 .h_full()
3182 }
3183 }
3184
3185 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
3186 cx.new(|_| TestView(state.clone())).into_any_element()
3187 });
3188
3189 assert_eq!(state.logical_scroll_top().item_ix, ITEMS);
3192
3193 let max_offset = state.max_offset_for_scrollbar();
3194 let scroll_offset = state.scroll_px_offset_for_scrollbar();
3195
3196 assert_eq!(
3197 -scroll_offset.y, max_offset.y,
3198 "scrollbar offset ({}) should equal max offset ({}) when list is pinned to bottom",
3199 -scroll_offset.y, max_offset.y,
3200 );
3201 }
3202
3203 #[gpui::test]
3207 fn test_follow_tail_reengages_when_scrolled_back_to_bottom(cx: &mut TestAppContext) {
3208 let cx = cx.add_empty_window();
3209
3210 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
3212
3213 struct TestView(ListState);
3214 impl Render for TestView {
3215 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3216 list(self.0.clone(), |_, _, _| {
3217 div().h(px(50.)).w_full().into_any()
3218 })
3219 .w_full()
3220 .h_full()
3221 }
3222 }
3223
3224 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3225
3226 state.set_follow_mode(FollowMode::Tail);
3227
3228 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3229 view.clone().into_any_element()
3230 });
3231 assert!(state.is_following_tail());
3232
3233 cx.simulate_event(ScrollWheelEvent {
3235 position: point(px(50.), px(100.)),
3236 delta: ScrollDelta::Pixels(point(px(0.), px(50.))),
3237 ..Default::default()
3238 });
3239 assert!(!state.is_following_tail());
3240
3241 cx.simulate_event(ScrollWheelEvent {
3243 position: point(px(50.), px(100.)),
3244 delta: ScrollDelta::Pixels(point(px(0.), px(-10000.))),
3245 ..Default::default()
3246 });
3247
3248 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3251 view.clone().into_any_element()
3252 });
3253 assert!(
3254 state.is_following_tail(),
3255 "follow_tail should re-engage after scrolling back to the bottom"
3256 );
3257 }
3258
3259 #[gpui::test]
3262 fn test_follow_tail_reengagement_not_fooled_by_unmeasured_items(cx: &mut TestAppContext) {
3263 let cx = cx.add_empty_window();
3264
3265 let state = ListState::new(20, crate::ListAlignment::Top, px(1000.));
3269
3270 struct TestView(ListState);
3271 impl Render for TestView {
3272 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3273 list(self.0.clone(), |_, _, _| {
3274 div().h(px(50.)).w_full().into_any()
3275 })
3276 .w_full()
3277 .h_full()
3278 }
3279 }
3280
3281 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3282
3283 state.set_follow_mode(FollowMode::Tail);
3284
3285 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3286 view.clone().into_any_element()
3287 });
3288 assert!(state.is_following_tail());
3289
3290 cx.simulate_event(ScrollWheelEvent {
3294 position: point(px(50.), px(100.)),
3295 delta: ScrollDelta::Pixels(point(px(0.), px(200.))),
3296 ..Default::default()
3297 });
3298 assert!(!state.is_following_tail());
3299
3300 state.remeasure_items(19..20);
3304
3305 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3311 view.clone().into_any_element()
3312 });
3313 assert!(
3314 !state.is_following_tail(),
3315 "follow_tail should not falsely re-engage due to an unmeasured item \
3316 reducing items.summary().height"
3317 );
3318 }
3319
3320 #[gpui::test]
3321 fn test_follow_tail_reengages_after_scrollbar_disengagement(cx: &mut TestAppContext) {
3322 let cx = cx.add_empty_window();
3323
3324 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3326
3327 struct TestView(ListState);
3328 impl Render for TestView {
3329 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3330 list(self.0.clone(), |_, _, _| {
3331 div().h(px(50.)).w_full().into_any()
3332 })
3333 .w_full()
3334 .h_full()
3335 }
3336 }
3337
3338 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3339
3340 state.set_follow_mode(FollowMode::Tail);
3341 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3342 view.clone().into_any_element()
3343 });
3344 assert!(state.is_following_tail());
3345
3346 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
3348 assert!(!state.is_following_tail());
3349
3350 state.set_offset_from_scrollbar(point(px(0.), px(-300.)));
3353 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3354 view.into_any_element()
3355 });
3356 assert!(
3357 state.is_following_tail(),
3358 "follow_tail should re-engage after scrolling back to the bottom via the scrollbar"
3359 );
3360 }
3361
3362 #[gpui::test]
3363 fn test_follow_tail_reengages_after_scrollbar_drag_to_bottom_while_growing(
3364 cx: &mut TestAppContext,
3365 ) {
3366 let cx = cx.add_empty_window();
3367
3368 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3369
3370 struct TestView(ListState);
3371 impl Render for TestView {
3372 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3373 list(self.0.clone(), |_, _, _| {
3374 div().h(px(50.)).w_full().into_any()
3375 })
3376 .w_full()
3377 .h_full()
3378 }
3379 }
3380
3381 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3382
3383 state.set_follow_mode(FollowMode::Tail);
3384 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3385 view.clone().into_any_element()
3386 });
3387 assert!(state.is_following_tail());
3388
3389 state.scrollbar_drag_started();
3390
3391 state.splice(10..10, 10);
3392 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3393 view.clone().into_any_element()
3394 });
3395
3396 state.set_offset_from_scrollbar(point(px(0.), px(-300.)));
3397 state.scrollbar_drag_ended();
3398
3399 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3400 view.into_any_element()
3401 });
3402
3403 assert!(
3404 state.is_following_tail(),
3405 "follow_tail should re-engage when the user drags the scrollbar to \
3406 the bottom of its track, even when content has grown during the drag \
3407 (so frozen_bottom < live_bottom)"
3408 );
3409 }
3410}