1use crate::{
11 AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
12 FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,
13 Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
14 Window, point, px, size,
15};
16use collections::VecDeque;
17use refineable::Refineable as _;
18use std::{cell::RefCell, ops::Range, rc::Rc};
19use sum_tree::{Bias, Dimensions, SumTree};
20
21type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static;
22
23pub fn list(
25 state: ListState,
26 render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static,
27) -> List {
28 List {
29 state,
30 render_item: Box::new(render_item),
31 style: StyleRefinement::default(),
32 sizing_behavior: ListSizingBehavior::default(),
33 }
34}
35
36pub struct List {
38 state: ListState,
39 render_item: Box<RenderItemFn>,
40 style: StyleRefinement,
41 sizing_behavior: ListSizingBehavior,
42}
43
44impl List {
45 pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
47 self.sizing_behavior = behavior;
48 self
49 }
50}
51
52#[derive(Clone)]
54pub struct ListState(Rc<RefCell<StateInner>>);
55
56impl std::fmt::Debug for ListState {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.write_str("ListState")
59 }
60}
61
62struct StateInner {
63 last_layout_bounds: Option<Bounds<Pixels>>,
64 last_padding: Option<Edges<Pixels>>,
65 items: SumTree<ListItem>,
66 logical_scroll_top: Option<ListOffset>,
67 alignment: ListAlignment,
68 overdraw: Pixels,
69 reset: bool,
70 #[allow(clippy::type_complexity)]
71 scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,
72 scrollbar_drag_start_height: Option<Pixels>,
73 measuring_behavior: ListMeasuringBehavior,
74 pending_scroll: Option<PendingScroll>,
75 follow_state: FollowState,
76}
77
78#[derive(Clone)]
80enum PendingScroll {
81 Absolute { item_ix: usize, offset: Pixels },
83 Proportional(PendingScrollFraction),
85}
86
87#[derive(Clone)]
90struct PendingScrollFraction {
91 item_ix: usize,
92 fraction: f32,
93}
94
95enum ScrollAnchor {
96 Absolute,
97 Proportional,
98}
99
100#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
102pub enum FollowMode {
103 #[default]
105 Normal,
106 Tail,
108}
109
110#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
111enum FollowState {
112 #[default]
113 Normal,
114 Tail {
115 is_following: bool,
116 },
117}
118
119impl FollowState {
120 fn is_following(&self) -> bool {
121 matches!(self, FollowState::Tail { is_following: true })
122 }
123
124 fn has_stopped_following(&self) -> bool {
125 matches!(
126 self,
127 FollowState::Tail {
128 is_following: false
129 }
130 )
131 }
132
133 fn start_following(&mut self) {
134 if let FollowState::Tail {
135 is_following: false,
136 } = self
137 {
138 *self = FollowState::Tail { is_following: true };
139 }
140 }
141
142 fn stop_following(&mut self) {
143 if let FollowState::Tail { is_following: true } = self {
144 *self = FollowState::Tail {
145 is_following: false,
146 };
147 }
148 }
149}
150
151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
153pub enum ListAlignment {
154 Top,
156 Bottom,
158}
159
160pub struct ListScrollEvent {
162 pub visible_range: Range<usize>,
164
165 pub count: usize,
167
168 pub is_scrolled: bool,
170
171 pub is_following_tail: bool,
173}
174
175#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
177pub enum ListSizingBehavior {
178 Infer,
180 #[default]
182 Auto,
183}
184
185#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
187pub enum ListMeasuringBehavior {
188 Measure(bool),
190 #[default]
192 Visible,
193}
194
195impl ListMeasuringBehavior {
196 fn reset(&mut self) {
197 match self {
198 ListMeasuringBehavior::Measure(has_measured) => *has_measured = false,
199 ListMeasuringBehavior::Visible => {}
200 }
201 }
202}
203
204#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
206pub enum ListHorizontalSizingBehavior {
207 #[default]
209 FitList,
210 Unconstrained,
212}
213
214struct LayoutItemsResponse {
215 max_item_width: Pixels,
216 scroll_top: ListOffset,
217 item_layouts: VecDeque<ItemLayout>,
218}
219
220struct ItemLayout {
221 index: usize,
222 element: AnyElement,
223 size: Size<Pixels>,
224}
225
226pub struct ListPrepaintState {
228 hitbox: Hitbox,
229 layout: LayoutItemsResponse,
230}
231
232#[derive(Clone)]
233enum ListItem {
234 Unmeasured {
235 size_hint: Option<Size<Pixels>>,
236 focus_handle: Option<FocusHandle>,
237 },
238 Measured {
239 size: Size<Pixels>,
240 focus_handle: Option<FocusHandle>,
241 },
242}
243
244impl ListItem {
245 fn size(&self) -> Option<Size<Pixels>> {
246 if let ListItem::Measured { size, .. } = self {
247 Some(*size)
248 } else {
249 None
250 }
251 }
252
253 fn size_hint(&self) -> Option<Size<Pixels>> {
254 match self {
255 ListItem::Measured { size, .. } => Some(*size),
256 ListItem::Unmeasured { size_hint, .. } => *size_hint,
257 }
258 }
259
260 fn focus_handle(&self) -> Option<FocusHandle> {
261 match self {
262 ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => {
263 focus_handle.clone()
264 }
265 }
266 }
267
268 fn contains_focused(&self, window: &Window, cx: &App) -> bool {
269 match self {
270 ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => {
271 focus_handle
272 .as_ref()
273 .is_some_and(|handle| handle.contains_focused(window, cx))
274 }
275 }
276 }
277}
278
279#[derive(Clone, Debug, Default, PartialEq)]
280struct ListItemSummary {
281 count: usize,
282 rendered_count: usize,
283 unrendered_count: usize,
284 height: Pixels,
285 has_focus_handles: bool,
286 has_unknown_height: bool,
287}
288
289#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
290struct Count(usize);
291
292#[derive(Clone, Debug, Default)]
293struct Height(Pixels);
294
295impl ListState {
296 pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self {
303 let this = Self(Rc::new(RefCell::new(StateInner {
304 last_layout_bounds: None,
305 last_padding: None,
306 items: SumTree::default(),
307 logical_scroll_top: None,
308 alignment,
309 overdraw,
310 scroll_handler: None,
311 reset: false,
312 scrollbar_drag_start_height: None,
313 measuring_behavior: ListMeasuringBehavior::default(),
314 pending_scroll: None,
315 follow_state: FollowState::default(),
316 })));
317 this.splice(0..0, item_count);
318 this
319 }
320
321 pub fn measure_all(self) -> Self {
325 self.0.borrow_mut().measuring_behavior = ListMeasuringBehavior::Measure(false);
326 self
327 }
328
329 pub fn with_uniform_item_height(self, height: Pixels) -> Self {
336 self.apply_uniform_item_height(height);
337 self
338 }
339
340 pub fn reset(&self, element_count: usize) {
344 let old_count = {
345 let state = &mut *self.0.borrow_mut();
346 state.reset = true;
347 state.measuring_behavior.reset();
348 state.logical_scroll_top = None;
349 state.pending_scroll = None;
350 state.scrollbar_drag_start_height = None;
351 state.items.summary().count
352 };
353
354 self.splice(0..old_count, element_count);
355 }
356
357 pub fn reset_with_uniform_height(&self, element_count: usize, height: Pixels) {
361 self.reset(element_count);
362 self.apply_uniform_item_height(height);
363 }
364
365 fn apply_uniform_item_height(&self, height: Pixels) {
366 let size_hint = Size {
367 width: px(0.),
368 height,
369 };
370 let mut state = self.0.borrow_mut();
371 let new_items = state
372 .items
373 .iter()
374 .map(|item| ListItem::Unmeasured {
375 size_hint: Some(item.size_hint().unwrap_or(size_hint)),
376 focus_handle: item.focus_handle(),
377 })
378 .collect::<Vec<_>>();
379 let mut tree = SumTree::default();
380 tree.extend(new_items, ());
381 state.items = tree;
382 }
383
384 pub fn remeasure(&self) {
386 let count = self.item_count();
387 self.remeasure_items_with_scroll_anchor(0..count, ScrollAnchor::Proportional);
388 }
389
390 pub fn remeasure_items(&self, range: Range<usize>) {
393 self.remeasure_items_with_scroll_anchor(range, ScrollAnchor::Absolute);
394 }
395
396 fn remeasure_items_with_scroll_anchor(&self, range: Range<usize>, scroll_anchor: ScrollAnchor) {
397 let state = &mut *self.0.borrow_mut();
398
399 if let Some(scroll_top) = state.logical_scroll_top
400 && range.contains(&scroll_top.item_ix)
401 {
402 state.pending_scroll = match scroll_anchor {
403 ScrollAnchor::Absolute => Some(PendingScroll::Absolute {
404 item_ix: scroll_top.item_ix,
405 offset: scroll_top.offset_in_item,
406 }),
407 ScrollAnchor::Proportional => {
408 let mut cursor = state.items.cursor::<Count>(());
409 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
410
411 cursor
412 .item()
413 .and_then(|item| {
414 item.size().map(|size| {
415 let fraction = if size.height > px(0.) {
416 (scroll_top.offset_in_item / size.height).clamp(0.0, 1.0)
417 } else {
418 0.0
419 };
420
421 PendingScroll::Proportional(PendingScrollFraction {
422 item_ix: scroll_top.item_ix,
423 fraction,
424 })
425 })
426 })
427 .or_else(|| state.pending_scroll.clone())
428 }
429 };
430 }
431
432 let new_items = {
433 let mut cursor = state.items.cursor::<Count>(());
434 let mut new_items = cursor.slice(&Count(range.start), Bias::Right);
435 let invalidated = cursor.slice(&Count(range.end), Bias::Right);
436 new_items.extend(
437 invalidated.iter().map(|item| ListItem::Unmeasured {
438 size_hint: item.size_hint(),
439 focus_handle: item.focus_handle(),
440 }),
441 (),
442 );
443 new_items.append(cursor.suffix(), ());
444 new_items
445 };
446 state.items = new_items;
447 state.measuring_behavior.reset();
448 }
449
450 pub fn item_count(&self) -> usize {
452 self.0.borrow().items.summary().count
453 }
454
455 pub fn is_scrolled_to_end(&self) -> Option<bool> {
458 let state = self.0.borrow();
459 let bounds = state.last_layout_bounds?;
460 let summary = state.items.summary();
461 if summary.has_unknown_height {
462 return None;
463 }
464 let padding = state.last_padding.unwrap_or_default();
465 let content_height = summary.height + padding.top + padding.bottom;
466 let scroll_max = (content_height - bounds.size.height).max(px(0.));
467 if scroll_max <= px(0.) {
468 return None;
469 }
470 let scroll_top = state.scroll_top(&state.logical_scroll_top());
471 Some(scroll_top >= scroll_max)
472 }
473
474 pub fn splice(&self, old_range: Range<usize>, count: usize) {
477 self.splice_focusable(old_range, (0..count).map(|_| None))
478 }
479
480 pub fn splice_focusable(
485 &self,
486 old_range: Range<usize>,
487 focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
488 ) {
489 let state = &mut *self.0.borrow_mut();
490
491 let mut old_items = state.items.cursor::<Count>(());
492 let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right);
493 old_items.seek_forward(&Count(old_range.end), Bias::Right);
494
495 let mut spliced_count = 0;
496 new_items.extend(
497 focus_handles.into_iter().map(|focus_handle| {
498 spliced_count += 1;
499 ListItem::Unmeasured {
500 size_hint: None,
501 focus_handle,
502 }
503 }),
504 (),
505 );
506 new_items.append(old_items.suffix(), ());
507 drop(old_items);
508 state.items = new_items;
509
510 if let Some(ListOffset {
511 item_ix,
512 offset_in_item,
513 }) = state.logical_scroll_top.as_mut()
514 {
515 if old_range.contains(item_ix) {
516 *item_ix = old_range.start;
517 *offset_in_item = px(0.);
518 } else if old_range.end <= *item_ix {
519 *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count;
520 }
521 }
522 }
523
524 pub fn set_scroll_handler(
526 &self,
527 handler: impl FnMut(&ListScrollEvent, &mut Window, &mut App) + 'static,
528 ) {
529 self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
530 }
531
532 pub fn logical_scroll_top(&self) -> ListOffset {
534 self.0.borrow().logical_scroll_top()
535 }
536
537 pub fn scroll_by(&self, distance: Pixels) {
539 if distance == px(0.) {
540 return;
541 }
542
543 let current_offset = self.logical_scroll_top();
544 let state = &mut *self.0.borrow_mut();
545
546 if distance < px(0.) {
547 state.follow_state.stop_following();
548 }
549
550 let mut cursor = state.items.cursor::<ListItemSummary>(());
551 cursor.seek(&Count(current_offset.item_ix), Bias::Right);
552
553 let start_pixel_offset = cursor.start().height + current_offset.offset_in_item;
554 let new_pixel_offset = (start_pixel_offset + distance).max(px(0.));
555 if new_pixel_offset > start_pixel_offset {
556 cursor.seek_forward(&Height(new_pixel_offset), Bias::Right);
557 } else {
558 cursor.seek(&Height(new_pixel_offset), Bias::Right);
559 }
560
561 let scroll_top = ListOffset {
562 item_ix: cursor.start().count,
563 offset_in_item: new_pixel_offset - cursor.start().height,
564 };
565 drop(cursor);
566 state.rebase_pending_scroll(scroll_top);
567 state.logical_scroll_top = Some(scroll_top);
568 }
569
570 pub fn scroll_to_end(&self) {
572 let state = &mut *self.0.borrow_mut();
573 let item_count = state.items.summary().count;
574 state.pending_scroll = None;
575 state.logical_scroll_top = Some(ListOffset {
576 item_ix: item_count,
577 offset_in_item: px(0.),
578 });
579 }
580
581 pub fn set_follow_mode(&self, mode: FollowMode) {
583 let state = &mut *self.0.borrow_mut();
584
585 match mode {
586 FollowMode::Normal => {
587 state.follow_state = FollowState::Normal;
588 }
589 FollowMode::Tail => {
590 state.follow_state = FollowState::Tail { is_following: true };
591 let item_count = state.items.summary().count;
592 state.pending_scroll = None;
593 state.logical_scroll_top = Some(ListOffset {
594 item_ix: item_count,
595 offset_in_item: px(0.),
596 });
597 }
598 }
599 }
600
601 pub fn set_follow_tail(&self, follow: bool) {
603 self.set_follow_mode(if follow {
604 FollowMode::Tail
605 } else {
606 FollowMode::Normal
607 });
608 }
609
610 pub fn is_following_tail(&self) -> bool {
612 self.0.borrow().follow_state.is_following()
613 }
614
615 pub fn scroll_to(&self, mut scroll_top: ListOffset) {
617 let state = &mut *self.0.borrow_mut();
618 let item_count = state.items.summary().count;
619 if scroll_top.item_ix >= item_count {
620 scroll_top.item_ix = item_count;
621 scroll_top.offset_in_item = px(0.);
622 }
623
624 if scroll_top.item_ix < item_count {
625 state.follow_state.stop_following();
626 }
627
628 state.rebase_pending_scroll(scroll_top);
629 state.logical_scroll_top = Some(scroll_top);
630 }
631
632 pub fn scroll_to_reveal_item(&self, ix: usize) {
634 let state = &mut *self.0.borrow_mut();
635
636 let mut scroll_top = state.logical_scroll_top();
637 let height = state
638 .last_layout_bounds
639 .map_or(px(0.), |bounds| bounds.size.height);
640 let padding = state.last_padding.unwrap_or_default();
641
642 if ix <= scroll_top.item_ix {
643 scroll_top.item_ix = ix;
644 scroll_top.offset_in_item = px(0.);
645 } else {
646 let mut cursor = state.items.cursor::<ListItemSummary>(());
647 cursor.seek(&Count(ix + 1), Bias::Right);
648 let bottom = cursor.start().height + padding.top;
649 let goal_top = px(0.).max(bottom - height + padding.bottom);
650
651 cursor.seek(&Height(goal_top), Bias::Left);
652 let start_ix = cursor.start().count;
653 let start_item_top = cursor.start().height;
654
655 if start_ix >= scroll_top.item_ix {
656 scroll_top.item_ix = start_ix;
657 scroll_top.offset_in_item = goal_top - start_item_top;
658 }
659 }
660
661 state.rebase_pending_scroll(scroll_top);
662 state.logical_scroll_top = Some(scroll_top);
663 }
664
665 pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
668 let state = &*self.0.borrow();
669
670 let bounds = state.last_layout_bounds.unwrap_or_default();
671 let scroll_top = state.logical_scroll_top();
672 if ix < scroll_top.item_ix {
673 return None;
674 }
675
676 let mut cursor = state.items.cursor::<Dimensions<Count, Height>>(());
677 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
678
679 let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item;
680
681 cursor.seek_forward(&Count(ix), Bias::Right);
682 if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
683 let &Dimensions(Count(count), Height(top), _) = cursor.start();
684 if count == ix {
685 let top = bounds.top() + top - scroll_top;
686 return Some(Bounds::from_corners(
687 point(bounds.left(), top),
688 point(bounds.right(), top + size.height),
689 ));
690 }
691 }
692 None
693 }
694
695 pub fn scrollbar_drag_started(&self) {
700 let mut state = self.0.borrow_mut();
701 state.scrollbar_drag_start_height = Some(state.items.summary().height);
702 }
703
704 pub fn scrollbar_drag_ended(&self) {
708 self.0.borrow_mut().scrollbar_drag_start_height.take();
709 }
710
711 pub fn is_scrollbar_dragging(&self) -> bool {
713 self.0.borrow().scrollbar_drag_start_height.is_some()
714 }
715
716 pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
718 self.0.borrow_mut().set_offset_from_scrollbar(point);
719 }
720
721 pub fn max_offset_for_scrollbar(&self) -> Size<Pixels> {
724 let state = self.0.borrow();
725 Size::new(Pixels::ZERO, state.max_scroll_offset())
726 }
727
728 pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
733 let state = &self.0.borrow();
734
735 if state.logical_scroll_top.is_none() && state.alignment == ListAlignment::Bottom {
736 return Point::new(px(0.), -state.max_scroll_offset());
737 }
738
739 let logical_scroll_top = state.logical_scroll_top();
740
741 let mut cursor = state.items.cursor::<ListItemSummary>(());
742 let summary: ListItemSummary =
743 cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right);
744 let offset = summary.height + logical_scroll_top.offset_in_item;
745
746 Point::new(px(0.), -offset)
747 }
748
749 pub fn viewport_bounds(&self) -> Bounds<Pixels> {
751 self.0.borrow().last_layout_bounds.unwrap_or_default()
752 }
753
754 pub fn item_is_above_viewport(&self, ix: usize) -> Option<bool> {
757 let viewport_bounds = self.viewport_bounds();
758 if viewport_bounds.size.height == px(0.0) {
759 return None;
760 }
761
762 let scroll_top = self.logical_scroll_top();
763 if ix < scroll_top.item_ix {
764 return Some(true);
765 }
766
767 let item_bounds = self.bounds_for_item(ix)?;
768 Some(item_bounds.bottom() <= viewport_bounds.top())
769 }
770
771 pub fn item_is_below_viewport(&self, ix: usize) -> Option<bool> {
774 let viewport_bounds = self.viewport_bounds();
775 if viewport_bounds.size.height == px(0.0) {
776 return None;
777 }
778
779 let scroll_top = self.logical_scroll_top();
780 if ix < scroll_top.item_ix {
781 return Some(false);
782 }
783
784 let item_bounds = self.bounds_for_item(ix)?;
785 Some(item_bounds.top() >= viewport_bounds.bottom())
786 }
787}
788
789impl StateInner {
790 fn rebase_pending_scroll(&mut self, scroll_top: ListOffset) {
794 let Some(pending) = self.pending_scroll.take() else {
795 return;
796 };
797 if scroll_top.item_ix >= self.items.summary().count {
798 return;
799 }
800
801 self.pending_scroll = match pending {
802 PendingScroll::Absolute { .. } => Some(PendingScroll::Absolute {
803 item_ix: scroll_top.item_ix,
804 offset: scroll_top.offset_in_item,
805 }),
806 PendingScroll::Proportional(_) => {
807 let mut cursor = self.items.cursor::<Count>(());
808 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
809 cursor
810 .item()
811 .and_then(|item| item.size_hint())
812 .filter(|size| size.height > px(0.))
813 .map(|size| {
814 PendingScroll::Proportional(PendingScrollFraction {
815 item_ix: scroll_top.item_ix,
816 fraction: (scroll_top.offset_in_item / size.height).clamp(0.0, 1.0),
817 })
818 })
819 }
820 };
821 }
822
823 fn max_scroll_offset(&self) -> Pixels {
824 let bounds = self.last_layout_bounds.unwrap_or_default();
825 let height = self
826 .scrollbar_drag_start_height
827 .unwrap_or_else(|| self.items.summary().height);
828 (height - bounds.size.height).max(px(0.))
829 }
830
831 fn visible_range(
832 items: &SumTree<ListItem>,
833 height: Pixels,
834 scroll_top: &ListOffset,
835 ) -> Range<usize> {
836 let mut cursor = items.cursor::<ListItemSummary>(());
837 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
838 let start_y = cursor.start().height + scroll_top.offset_in_item;
839 cursor.seek_forward(&Height(start_y + height), Bias::Left);
840 scroll_top.item_ix..cursor.start().count + 1
841 }
842
843 fn scroll(
844 &mut self,
845 scroll_top: &ListOffset,
846 height: Pixels,
847 delta: Point<Pixels>,
848 current_view: EntityId,
849 window: &mut Window,
850 cx: &mut App,
851 ) {
852 if self.reset {
855 return;
856 }
857
858 let padding = self.last_padding.unwrap_or_default();
859 let scroll_max =
860 (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
861 let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
862 .max(px(0.))
863 .min(scroll_max);
864
865 let mut cursor = self.items.cursor::<ListItemSummary>(());
866 cursor.seek(&Height(new_scroll_top), Bias::Right);
867 let scroll_top_after_event = ListOffset {
868 item_ix: cursor.start().count,
869 offset_in_item: new_scroll_top - cursor.start().height,
870 };
871 drop(cursor);
872
873 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
874 self.pending_scroll = None;
875 self.logical_scroll_top = None;
876 } else {
877 self.rebase_pending_scroll(scroll_top_after_event);
878 self.logical_scroll_top = Some(scroll_top_after_event);
879 }
880
881 if delta.y > px(0.) {
882 self.follow_state.stop_following();
883 }
884
885 if let Some(handler) = self.scroll_handler.as_mut() {
886 let visible_range = Self::visible_range(&self.items, height, &scroll_top_after_event);
887 let event = ListScrollEvent {
888 visible_range,
889 count: self.items.summary().count,
890 is_scrolled: self.logical_scroll_top.is_some(),
891 is_following_tail: self.follow_state.is_following(),
892 };
893 handler(&event, window, cx);
894 }
895
896 cx.notify(current_view);
897 }
898
899 fn logical_scroll_top(&self) -> ListOffset {
900 self.logical_scroll_top
901 .unwrap_or_else(|| match self.alignment {
902 ListAlignment::Top => ListOffset {
903 item_ix: 0,
904 offset_in_item: px(0.),
905 },
906 ListAlignment::Bottom => ListOffset {
907 item_ix: self.items.summary().count,
908 offset_in_item: px(0.),
909 },
910 })
911 }
912
913 fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
914 let mut cursor = self.items.cursor::<ListItemSummary>(());
915 cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right);
916 cursor.start().height + logical_scroll_top.offset_in_item
917 }
918
919 fn layout_all_items(
920 &mut self,
921 available_width: Pixels,
922 render_item: &mut RenderItemFn,
923 window: &mut Window,
924 cx: &mut App,
925 ) {
926 match &mut self.measuring_behavior {
927 ListMeasuringBehavior::Visible => return,
928 ListMeasuringBehavior::Measure(has_measured) => {
929 if *has_measured {
930 return;
931 }
932 *has_measured = true;
933 }
934 }
935
936 let available_item_space = size(
937 AvailableSpace::Definite(available_width),
938 AvailableSpace::MinContent,
939 );
940
941 let measured_items = self.items.iter().enumerate().map(|(ix, item)| {
942 let size = item.size().unwrap_or_else(|| {
943 let mut element = render_item(ix, window, cx);
944 element.layout_as_root(available_item_space, window, cx)
945 });
946
947 ListItem::Measured {
948 size,
949 focus_handle: item.focus_handle(),
950 }
951 });
952
953 self.items = SumTree::from_iter(measured_items, ());
954 }
955
956 fn layout_items(
957 &mut self,
958 available_width: Option<Pixels>,
959 available_height: Pixels,
960 padding: &Edges<Pixels>,
961 render_item: &mut RenderItemFn,
962 window: &mut Window,
963 cx: &mut App,
964 ) -> LayoutItemsResponse {
965 let old_items = self.items.clone();
966 let mut measured_items = VecDeque::new();
967 let mut item_layouts = VecDeque::new();
968 let mut rendered_height = padding.top;
969 let mut max_item_width = px(0.);
970 let mut scroll_top = self.logical_scroll_top();
971
972 if self.follow_state.is_following() {
973 scroll_top = ListOffset {
974 item_ix: self.items.summary().count,
975 offset_in_item: px(0.),
976 };
977 self.logical_scroll_top = Some(scroll_top);
978 }
979
980 let mut rendered_focused_item = false;
981
982 let available_item_space = size(
983 crate::taffy::available_space_for_optional_width(available_width),
984 AvailableSpace::MinContent,
985 );
986
987 let mut cursor = old_items.cursor::<Count>(());
988
989 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
991 for (ix, item) in cursor.by_ref().enumerate() {
992 let visible_height = rendered_height - scroll_top.offset_in_item;
993 if visible_height >= available_height + self.overdraw {
994 break;
995 }
996
997 let mut size = item.size();
999
1000 if visible_height < available_height || size.is_none() {
1002 let item_index = scroll_top.item_ix + ix;
1003 let mut element = render_item(item_index, window, cx);
1004 let element_size = element.layout_as_root(available_item_space, window, cx);
1005 size = Some(element_size);
1006
1007 if ix == 0
1008 && let Some(pending_scroll) = self.pending_scroll.take()
1009 {
1010 match pending_scroll {
1011 PendingScroll::Absolute { item_ix, offset }
1012 if item_ix == scroll_top.item_ix =>
1013 {
1014 scroll_top.offset_in_item = offset.min(element_size.height);
1015 self.logical_scroll_top = Some(scroll_top);
1016 }
1017 PendingScroll::Proportional(pending_scroll)
1018 if pending_scroll.item_ix == scroll_top.item_ix =>
1019 {
1020 scroll_top.offset_in_item =
1021 element_size.height * pending_scroll.fraction;
1022 self.logical_scroll_top = Some(scroll_top);
1023 }
1024 _ => {}
1025 }
1026 }
1027
1028 if visible_height < available_height {
1029 item_layouts.push_back(ItemLayout {
1030 index: item_index,
1031 element,
1032 size: element_size,
1033 });
1034 if item.contains_focused(window, cx) {
1035 rendered_focused_item = true;
1036 }
1037 }
1038 }
1039
1040 let size = size.unwrap();
1041 rendered_height += size.height;
1042 max_item_width = max_item_width.max(size.width);
1043 measured_items.push_back(ListItem::Measured {
1044 size,
1045 focus_handle: item.focus_handle(),
1046 });
1047 }
1048 rendered_height += padding.bottom;
1049
1050 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
1052
1053 if rendered_height - scroll_top.offset_in_item < available_height {
1056 while rendered_height < available_height {
1057 cursor.prev();
1058 if let Some(item) = cursor.item() {
1059 let item_index = cursor.start().0;
1060 let mut element = render_item(item_index, window, cx);
1061 let element_size = element.layout_as_root(available_item_space, window, cx);
1062 let focus_handle = item.focus_handle();
1063 rendered_height += element_size.height;
1064 measured_items.push_front(ListItem::Measured {
1065 size: element_size,
1066 focus_handle,
1067 });
1068 item_layouts.push_front(ItemLayout {
1069 index: item_index,
1070 element,
1071 size: element_size,
1072 });
1073 if item.contains_focused(window, cx) {
1074 rendered_focused_item = true;
1075 }
1076 } else {
1077 break;
1078 }
1079 }
1080
1081 scroll_top = ListOffset {
1082 item_ix: cursor.start().0,
1083 offset_in_item: rendered_height - available_height,
1084 };
1085
1086 match self.alignment {
1087 ListAlignment::Top => {
1088 scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
1089 self.logical_scroll_top = Some(scroll_top);
1090 }
1091 ListAlignment::Bottom => {
1092 scroll_top = ListOffset {
1093 item_ix: cursor.start().0,
1094 offset_in_item: rendered_height - available_height,
1095 };
1096 self.logical_scroll_top = None;
1097 }
1098 };
1099 }
1100
1101 let mut leading_overdraw = scroll_top.offset_in_item;
1103 while leading_overdraw < self.overdraw {
1104 cursor.prev();
1105 if let Some(item) = cursor.item() {
1106 let size = if let ListItem::Measured { size, .. } = item {
1107 *size
1108 } else {
1109 let mut element = render_item(cursor.start().0, window, cx);
1110 element.layout_as_root(available_item_space, window, cx)
1111 };
1112
1113 leading_overdraw += size.height;
1114 measured_items.push_front(ListItem::Measured {
1115 size,
1116 focus_handle: item.focus_handle(),
1117 });
1118 } else {
1119 break;
1120 }
1121 }
1122
1123 let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
1124 let mut cursor = old_items.cursor::<Count>(());
1125 let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right);
1126 new_items.extend(measured_items, ());
1127 cursor.seek(&Count(measured_range.end), Bias::Right);
1128 new_items.append(cursor.suffix(), ());
1129 self.items = new_items;
1130
1131 if self.follow_state.has_stopped_following() {
1132 let padding = self.last_padding.unwrap_or_default();
1133 let total_height = self.items.summary().height + padding.top + padding.bottom;
1134 let scroll_offset = self.scroll_top(&scroll_top);
1135 if scroll_offset + available_height >= total_height - px(1.0) {
1136 self.follow_state.start_following();
1137 }
1138 }
1139
1140 if !rendered_focused_item {
1144 let mut cursor = self
1145 .items
1146 .filter::<_, Count>((), |summary| summary.has_focus_handles);
1147 cursor.next();
1148 while let Some(item) = cursor.item() {
1149 if item.contains_focused(window, cx) {
1150 let item_index = cursor.start().0;
1151 let mut element = render_item(cursor.start().0, window, cx);
1152 let size = element.layout_as_root(available_item_space, window, cx);
1153 item_layouts.push_back(ItemLayout {
1154 index: item_index,
1155 element,
1156 size,
1157 });
1158 break;
1159 }
1160 cursor.next();
1161 }
1162 }
1163
1164 LayoutItemsResponse {
1165 max_item_width,
1166 scroll_top,
1167 item_layouts,
1168 }
1169 }
1170
1171 fn prepaint_items(
1172 &mut self,
1173 bounds: Bounds<Pixels>,
1174 padding: Edges<Pixels>,
1175 autoscroll: bool,
1176 render_item: &mut RenderItemFn,
1177 window: &mut Window,
1178 cx: &mut App,
1179 ) -> Result<LayoutItemsResponse, ListOffset> {
1180 window.transact(|window| {
1181 if let ListMeasuringBehavior::Measure(false) = self.measuring_behavior {
1182 self.layout_all_items(bounds.size.width, render_item, window, cx);
1183 }
1184
1185 let mut layout_response = self.layout_items(
1186 Some(bounds.size.width),
1187 bounds.size.height,
1188 &padding,
1189 render_item,
1190 window,
1191 cx,
1192 );
1193
1194 window.take_autoscroll();
1196
1197 if bounds.size.height > padding.top + padding.bottom {
1199 let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
1200 item_origin.y -= layout_response.scroll_top.offset_in_item;
1201 for item in &mut layout_response.item_layouts {
1202 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1203 item.element.prepaint_at(item_origin, window, cx);
1204 });
1205
1206 if let Some(autoscroll_bounds) = window.take_autoscroll()
1207 && autoscroll
1208 {
1209 if autoscroll_bounds.top() < bounds.top() {
1210 return Err(ListOffset {
1211 item_ix: item.index,
1212 offset_in_item: autoscroll_bounds.top() - item_origin.y,
1213 });
1214 } else if autoscroll_bounds.bottom() > bounds.bottom() {
1215 let mut cursor = self.items.cursor::<Count>(());
1216 cursor.seek(&Count(item.index), Bias::Right);
1217 let mut height = bounds.size.height - padding.top - padding.bottom;
1218
1219 height -= autoscroll_bounds.bottom() - item_origin.y;
1221
1222 while height > Pixels::ZERO {
1224 cursor.prev();
1225 let Some(item) = cursor.item() else { break };
1226
1227 let size = item.size().unwrap_or_else(|| {
1228 let mut item = render_item(cursor.start().0, window, cx);
1229 let item_available_size =
1230 size(bounds.size.width.into(), AvailableSpace::MinContent);
1231 item.layout_as_root(item_available_size, window, cx)
1232 });
1233 height -= size.height;
1234 }
1235
1236 return Err(ListOffset {
1237 item_ix: cursor.start().0,
1238 offset_in_item: if height < Pixels::ZERO {
1239 -height
1240 } else {
1241 Pixels::ZERO
1242 },
1243 });
1244 }
1245 }
1246
1247 item_origin.y += item.size.height;
1248 }
1249 } else {
1250 layout_response.item_layouts.clear();
1251 }
1252
1253 Ok(layout_response)
1254 })
1255 }
1256
1257 fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
1260 let Some(bounds) = self.last_layout_bounds else {
1261 return;
1262 };
1263 let height = bounds.size.height;
1264
1265 let padding = self.last_padding.unwrap_or_default();
1266 let content_height = self
1267 .scrollbar_drag_start_height
1268 .unwrap_or_else(|| self.items.summary().height);
1269 let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
1270 let new_scroll_top = (-point.y).max(px(0.)).min(scroll_max);
1271
1272 let dragged_to_end =
1273 scroll_max > px(0.) && new_scroll_top >= (scroll_max - px(1.)).max(px(0.));
1274 if dragged_to_end && matches!(self.follow_state, FollowState::Tail { .. }) {
1275 self.follow_state = FollowState::Tail { is_following: true };
1276 let item_count = self.items.summary().count;
1277 self.pending_scroll = None;
1278 self.logical_scroll_top = Some(ListOffset {
1279 item_ix: item_count,
1280 offset_in_item: px(0.),
1281 });
1282 return;
1283 }
1284
1285 self.follow_state.stop_following();
1286
1287 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
1288 self.pending_scroll = None;
1289 self.logical_scroll_top = None;
1290 } else {
1291 let mut cursor = self.items.cursor::<ListItemSummary>(());
1292 cursor.seek(&Height(new_scroll_top), Bias::Right);
1293
1294 let scroll_top = ListOffset {
1295 item_ix: cursor.start().count,
1296 offset_in_item: new_scroll_top - cursor.start().height,
1297 };
1298 drop(cursor);
1299 self.rebase_pending_scroll(scroll_top);
1300 self.logical_scroll_top = Some(scroll_top);
1301 }
1302 }
1303}
1304
1305impl std::fmt::Debug for ListItem {
1306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1307 match self {
1308 Self::Unmeasured { .. } => write!(f, "Unrendered"),
1309 Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
1310 }
1311 }
1312}
1313
1314#[derive(Debug, Clone, Copy, Default)]
1317pub struct ListOffset {
1318 pub item_ix: usize,
1320 pub offset_in_item: Pixels,
1322}
1323
1324impl Element for List {
1325 type RequestLayoutState = ();
1326 type PrepaintState = ListPrepaintState;
1327
1328 fn id(&self) -> Option<crate::ElementId> {
1329 None
1330 }
1331
1332 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1333 None
1334 }
1335
1336 fn request_layout(
1337 &mut self,
1338 _id: Option<&GlobalElementId>,
1339 _inspector_id: Option<&InspectorElementId>,
1340 window: &mut Window,
1341 cx: &mut App,
1342 ) -> (crate::LayoutId, Self::RequestLayoutState) {
1343 let layout_id = match self.sizing_behavior {
1344 ListSizingBehavior::Infer => {
1345 let mut style = Style::default();
1346 style.overflow.y = Overflow::Scroll;
1347 style.refine(&self.style);
1348 window.with_text_style(style.text_style().cloned(), |window| {
1349 let state = &mut *self.state.0.borrow_mut();
1350
1351 let available_height = if let Some(last_bounds) = state.last_layout_bounds {
1352 last_bounds.size.height
1353 } else {
1354 state.overdraw
1357 };
1358 let padding = style.padding.to_pixels(
1359 state.last_layout_bounds.unwrap_or_default().size.into(),
1360 window.rem_size(),
1361 );
1362
1363 let layout_response = state.layout_items(
1364 None,
1365 available_height,
1366 &padding,
1367 &mut self.render_item,
1368 window,
1369 cx,
1370 );
1371 let max_element_width = layout_response.max_item_width;
1372
1373 let summary = state.items.summary();
1374 let total_height = summary.height;
1375
1376 window.request_measured_layout(
1377 style,
1378 move |known_dimensions, available_space, _window, _cx| {
1379 let width =
1380 known_dimensions
1381 .width
1382 .unwrap_or(match available_space.width {
1383 AvailableSpace::Definite(x) => x,
1384 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1385 max_element_width
1386 }
1387 });
1388 let height = match available_space.height {
1389 AvailableSpace::Definite(height) => total_height.min(height),
1390 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1391 total_height
1392 }
1393 };
1394 size(width, height)
1395 },
1396 )
1397 })
1398 }
1399 ListSizingBehavior::Auto => {
1400 let mut style = Style::default();
1401 style.refine(&self.style);
1402 window.with_text_style(style.text_style().cloned(), |window| {
1403 window.request_layout(style, None, cx)
1404 })
1405 }
1406 };
1407 (layout_id, ())
1408 }
1409
1410 fn prepaint(
1411 &mut self,
1412 _id: Option<&GlobalElementId>,
1413 _inspector_id: Option<&InspectorElementId>,
1414 bounds: Bounds<Pixels>,
1415 _: &mut Self::RequestLayoutState,
1416 window: &mut Window,
1417 cx: &mut App,
1418 ) -> ListPrepaintState {
1419 let state = &mut *self.state.0.borrow_mut();
1420 state.reset = false;
1421
1422 let mut style = Style::default();
1423 style.refine(&self.style);
1424
1425 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1426
1427 if state
1429 .last_layout_bounds
1430 .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width)
1431 {
1432 let new_items = SumTree::from_iter(
1433 state.items.iter().map(|item| ListItem::Unmeasured {
1434 size_hint: item.size_hint(),
1435 focus_handle: item.focus_handle(),
1436 }),
1437 (),
1438 );
1439
1440 state.items = new_items;
1441 state.measuring_behavior.reset();
1442 }
1443
1444 let padding = style
1445 .padding
1446 .to_pixels(bounds.size.into(), window.rem_size());
1447 let layout =
1448 match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) {
1449 Ok(layout) => layout,
1450 Err(autoscroll_request) => {
1451 state.logical_scroll_top = Some(autoscroll_request);
1452 state
1453 .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx)
1454 .unwrap()
1455 }
1456 };
1457
1458 state.last_layout_bounds = Some(bounds);
1459 state.last_padding = Some(padding);
1460 ListPrepaintState { hitbox, layout }
1461 }
1462
1463 fn paint(
1464 &mut self,
1465 _id: Option<&GlobalElementId>,
1466 _inspector_id: Option<&InspectorElementId>,
1467 bounds: Bounds<crate::Pixels>,
1468 _: &mut Self::RequestLayoutState,
1469 prepaint: &mut Self::PrepaintState,
1470 window: &mut Window,
1471 cx: &mut App,
1472 ) {
1473 let current_view = window.current_view();
1474 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1475 for item in &mut prepaint.layout.item_layouts {
1476 item.element.paint(window, cx);
1477 }
1478 });
1479
1480 let list_state = self.state.clone();
1481 let height = bounds.size.height;
1482 let scroll_top = prepaint.layout.scroll_top;
1483 let hitbox_id = prepaint.hitbox.id;
1484 let mut accumulated_scroll_delta = ScrollDelta::default();
1485 window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
1486 if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) {
1487 accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
1488 let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
1489 list_state.0.borrow_mut().scroll(
1490 &scroll_top,
1491 height,
1492 pixel_delta,
1493 current_view,
1494 window,
1495 cx,
1496 )
1497 }
1498 });
1499 }
1500}
1501
1502impl IntoElement for List {
1503 type Element = Self;
1504
1505 fn into_element(self) -> Self::Element {
1506 self
1507 }
1508}
1509
1510impl Styled for List {
1511 fn style(&mut self) -> &mut StyleRefinement {
1512 &mut self.style
1513 }
1514}
1515
1516impl sum_tree::Item for ListItem {
1517 type Summary = ListItemSummary;
1518
1519 fn summary(&self, _: ()) -> Self::Summary {
1520 match self {
1521 ListItem::Unmeasured {
1522 size_hint,
1523 focus_handle,
1524 } => ListItemSummary {
1525 count: 1,
1526 rendered_count: 0,
1527 unrendered_count: 1,
1528 height: size_hint.map_or(px(0.), |size| size.height),
1529 has_focus_handles: focus_handle.is_some(),
1530 has_unknown_height: size_hint.is_none(),
1531 },
1532 ListItem::Measured {
1533 size, focus_handle, ..
1534 } => ListItemSummary {
1535 count: 1,
1536 rendered_count: 1,
1537 unrendered_count: 0,
1538 height: size.height,
1539 has_focus_handles: focus_handle.is_some(),
1540 has_unknown_height: false,
1541 },
1542 }
1543 }
1544}
1545
1546impl sum_tree::ContextLessSummary for ListItemSummary {
1547 fn zero() -> Self {
1548 Default::default()
1549 }
1550
1551 fn add_summary(&mut self, summary: &Self) {
1552 self.count += summary.count;
1553 self.rendered_count += summary.rendered_count;
1554 self.unrendered_count += summary.unrendered_count;
1555 self.height += summary.height;
1556 self.has_focus_handles |= summary.has_focus_handles;
1557 self.has_unknown_height |= summary.has_unknown_height;
1558 }
1559}
1560
1561impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
1562 fn zero(_cx: ()) -> Self {
1563 Default::default()
1564 }
1565
1566 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1567 self.0 += summary.count;
1568 }
1569}
1570
1571impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
1572 fn zero(_cx: ()) -> Self {
1573 Default::default()
1574 }
1575
1576 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1577 self.0 += summary.height;
1578 }
1579}
1580
1581impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Count {
1582 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1583 self.0.partial_cmp(&other.count).unwrap()
1584 }
1585}
1586
1587impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height {
1588 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1589 self.0.partial_cmp(&other.height).unwrap()
1590 }
1591}
1592
1593#[cfg(test)]
1594mod test {
1595
1596 use gpui::{ScrollDelta, ScrollWheelEvent};
1597
1598 use crate::{
1599 self as gpui, AppContext, Context, Element, IntoElement, ListState, Render, Styled,
1600 TestAppContext, Window, div, list, point, px, size,
1601 };
1602
1603 struct TestListView(ListState);
1604
1605 impl Render for TestListView {
1606 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1607 list(self.0.clone(), |_, _, _| {
1608 div().h(px(20.)).w_full().into_any()
1609 })
1610 .w_full()
1611 .h_full()
1612 }
1613 }
1614
1615 #[gpui::test]
1616 fn test_uniform_item_height_populates_size_hints(_cx: &mut TestAppContext) {
1617 let state =
1618 ListState::new(5, crate::ListAlignment::Top, px(0.)).with_uniform_item_height(px(20.));
1619
1620 let summary = state.0.borrow().items.summary().clone();
1621 assert_eq!(summary.count, 5);
1622 assert_eq!(summary.rendered_count, 0);
1623 assert_eq!(summary.unrendered_count, 5);
1624 assert_eq!(summary.height, px(100.));
1625 assert_eq!(state.max_offset_for_scrollbar().height, px(100.));
1626
1627 state.reset_with_uniform_height(3, px(10.));
1628
1629 let summary = state.0.borrow().items.summary().clone();
1630 assert_eq!(summary.count, 3);
1631 assert_eq!(summary.rendered_count, 0);
1632 assert_eq!(summary.unrendered_count, 3);
1633 assert_eq!(summary.height, px(30.));
1634 assert_eq!(state.max_offset_for_scrollbar().height, px(30.));
1635 }
1636
1637 #[gpui::test]
1638 fn test_is_scrolled_to_end_is_unknown_until_item_heights_are_known(cx: &mut TestAppContext) {
1639 let cx = cx.add_empty_window();
1640 let state = ListState::new(5, crate::ListAlignment::Top, px(0.));
1641
1642 cx.draw(point(px(0.), px(0.)), size(px(100.), px(40.)), |_, cx| {
1643 cx.new(|_| TestListView(state.clone())).into_any_element()
1644 });
1645
1646 assert_eq!(state.is_scrolled_to_end(), None);
1647 }
1648
1649 #[gpui::test]
1650 fn test_is_scrolled_to_end_reports_when_item_heights_are_known(cx: &mut TestAppContext) {
1651 let cx = cx.add_empty_window();
1652 let state = ListState::new(5, crate::ListAlignment::Top, px(0.)).measure_all();
1653
1654 cx.draw(point(px(0.), px(0.)), size(px(100.), px(40.)), |_, cx| {
1655 cx.new(|_| TestListView(state.clone())).into_any_element()
1656 });
1657
1658 assert_eq!(state.is_scrolled_to_end(), Some(false));
1659
1660 state.scroll_to_end();
1661
1662 assert_eq!(state.is_scrolled_to_end(), Some(true));
1663 }
1664
1665 #[gpui::test]
1666 fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
1667 use crate::{
1668 AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
1669 list, point, px, size,
1670 };
1671
1672 let cx = cx.add_empty_window();
1673
1674 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1675
1676 state.scroll_to(gpui::ListOffset {
1678 item_ix: 0,
1679 offset_in_item: px(0.0),
1680 });
1681
1682 struct TestView(ListState);
1683 impl Render for TestView {
1684 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1685 list(self.0.clone(), |_, _, _| {
1686 div().h(px(10.)).w_full().into_any()
1687 })
1688 .w_full()
1689 .h_full()
1690 }
1691 }
1692
1693 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1695 cx.new(|_| TestView(state.clone()))
1696 });
1697
1698 state.reset(5);
1700
1701 cx.simulate_event(ScrollWheelEvent {
1703 position: point(px(1.), px(1.)),
1704 delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
1705 ..Default::default()
1706 });
1707
1708 assert_eq!(state.logical_scroll_top().item_ix, 0);
1710 assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
1711 }
1712
1713 #[gpui::test]
1714 fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) {
1715 use crate::{
1716 AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
1717 list, point, px, size,
1718 };
1719
1720 let cx = cx.add_empty_window();
1721
1722 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1723
1724 struct TestView(ListState);
1725 impl Render for TestView {
1726 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1727 list(self.0.clone(), |_, _, _| {
1728 div().h(px(20.)).w_full().into_any()
1729 })
1730 .w_full()
1731 .h_full()
1732 }
1733 }
1734
1735 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
1737 cx.new(|_| TestView(state.clone()))
1738 });
1739
1740 state.scroll_by(px(30.));
1742
1743 let offset = state.logical_scroll_top();
1745 assert_eq!(offset.item_ix, 1);
1746 assert_eq!(offset.offset_in_item, px(10.));
1747
1748 state.scroll_by(px(-30.));
1750
1751 let offset = state.logical_scroll_top();
1753 assert_eq!(offset.item_ix, 0);
1754 assert_eq!(offset.offset_in_item, px(0.));
1755
1756 state.scroll_by(px(0.));
1758 let offset = state.logical_scroll_top();
1759 assert_eq!(offset.item_ix, 0);
1760 assert_eq!(offset.offset_in_item, px(0.));
1761 }
1762
1763 #[gpui::test]
1764 fn test_item_viewport_queries_return_none_before_layout(_cx: &mut TestAppContext) {
1765 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1766
1767 assert_eq!(state.item_is_above_viewport(0), None);
1768 assert_eq!(state.item_is_below_viewport(0), None);
1769 }
1770
1771 #[gpui::test]
1772 fn test_item_viewport_queries_before_logical_scroll_top(cx: &mut TestAppContext) {
1773 let cx = cx.add_empty_window();
1774 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1775
1776 state.scroll_to(gpui::ListOffset {
1777 item_ix: 2,
1778 offset_in_item: px(0.),
1779 });
1780 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1781 cx.new(|_| TestListView(state.clone())).into_any_element()
1782 });
1783
1784 assert_eq!(state.item_is_above_viewport(1), Some(true));
1785 assert_eq!(state.item_is_below_viewport(1), Some(false));
1786 }
1787
1788 #[gpui::test]
1789 fn test_item_viewport_queries_measured_item_inside_viewport(cx: &mut TestAppContext) {
1790 let cx = cx.add_empty_window();
1791 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1792
1793 state.scroll_to(gpui::ListOffset {
1794 item_ix: 2,
1795 offset_in_item: px(0.),
1796 });
1797 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1798 cx.new(|_| TestListView(state.clone())).into_any_element()
1799 });
1800
1801 assert_eq!(state.item_is_above_viewport(2), Some(false));
1802 assert_eq!(state.item_is_below_viewport(2), Some(false));
1803 }
1804
1805 #[gpui::test]
1806 fn test_item_viewport_queries_measured_item_above_viewport(cx: &mut TestAppContext) {
1807 let cx = cx.add_empty_window();
1808 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1809
1810 state.scroll_to(gpui::ListOffset {
1811 item_ix: 2,
1812 offset_in_item: px(20.),
1813 });
1814 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1815 cx.new(|_| TestListView(state.clone())).into_any_element()
1816 });
1817
1818 assert_eq!(state.item_is_above_viewport(2), Some(true));
1819 assert_eq!(state.item_is_below_viewport(2), Some(false));
1820 }
1821
1822 #[gpui::test]
1823 fn test_item_viewport_queries_measured_item_below_viewport(cx: &mut TestAppContext) {
1824 let cx = cx.add_empty_window();
1825 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1826
1827 state.scroll_to(gpui::ListOffset {
1828 item_ix: 2,
1829 offset_in_item: px(0.),
1830 });
1831 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1832 cx.new(|_| TestListView(state.clone())).into_any_element()
1833 });
1834
1835 assert_eq!(state.item_is_above_viewport(3), Some(false));
1836 assert_eq!(state.item_is_below_viewport(3), Some(true));
1837 }
1838
1839 #[gpui::test]
1840 fn test_item_viewport_queries_after_scroll_to_end_before_layout(cx: &mut TestAppContext) {
1841 let cx = cx.add_empty_window();
1842 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1843
1844 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1845 cx.new(|_| TestListView(state.clone())).into_any_element()
1846 });
1847
1848 state.scroll_to_end();
1849
1850 assert_eq!(state.logical_scroll_top().item_ix, state.item_count());
1851 assert_eq!(state.item_is_above_viewport(0), Some(true));
1852 assert_eq!(state.item_is_below_viewport(0), Some(false));
1853 }
1854
1855 #[gpui::test]
1856 fn test_scroll_handler_reports_visible_range_after_scroll(cx: &mut TestAppContext) {
1857 use crate::{
1858 AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
1859 list, point, px, size,
1860 };
1861 use std::{cell::RefCell, rc::Rc};
1862
1863 let cx = cx.add_empty_window();
1864
1865 let state = ListState::new(5, crate::ListAlignment::Top, px(0.)).measure_all();
1866 let visible_range = Rc::new(RefCell::new(None));
1867 let observed_visible_range = visible_range.clone();
1868 state.set_scroll_handler(move |event, _, _| {
1869 *observed_visible_range.borrow_mut() = Some(event.visible_range.clone());
1870 });
1871
1872 struct TestView(ListState);
1873 impl Render for TestView {
1874 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1875 list(self.0.clone(), |_, _, _| {
1876 div().h(px(20.)).w_full().into_any()
1877 })
1878 .w_full()
1879 .h_full()
1880 }
1881 }
1882
1883 cx.draw(point(px(0.), px(0.)), size(px(100.), px(40.)), |_, cx| {
1884 cx.new(|_| TestView(state.clone()))
1885 });
1886
1887 cx.simulate_event(ScrollWheelEvent {
1888 position: point(px(1.), px(1.)),
1889 delta: ScrollDelta::Pixels(point(px(0.), px(-20.))),
1890 ..Default::default()
1891 });
1892
1893 let offset = state.logical_scroll_top();
1894 assert_eq!(offset.item_ix, 1);
1895 assert_eq!(offset.offset_in_item, px(0.));
1896
1897 let visible_range = visible_range.borrow().clone().unwrap();
1898 assert_eq!(visible_range.start, offset.item_ix);
1899 }
1900
1901 #[gpui::test]
1902 fn test_remeasure_items_preserves_size_hints_for_scrollbar(cx: &mut TestAppContext) {
1903 use crate::{
1904 AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
1905 list, point, px, size,
1906 };
1907
1908 let cx = cx.add_empty_window();
1909
1910 let state = ListState::new(5, crate::ListAlignment::Top, px(0.)).measure_all();
1911
1912 struct TestView(ListState);
1913 impl Render for TestView {
1914 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1915 list(self.0.clone(), |_, _, _| {
1916 div().h(px(20.)).w_full().into_any()
1917 })
1918 .w_full()
1919 .h_full()
1920 }
1921 }
1922
1923 cx.draw(point(px(0.), px(0.)), size(px(100.), px(40.)), |_, cx| {
1924 cx.new(|_| TestView(state.clone()))
1925 });
1926
1927 assert_eq!(state.max_offset_for_scrollbar().height, px(60.));
1928
1929 state.remeasure_items(0..5);
1930
1931 let summary = state.0.borrow().items.summary().clone();
1932 assert_eq!(summary.rendered_count, 0);
1933 assert_eq!(summary.unrendered_count, 5);
1934 assert_eq!(summary.height, px(100.));
1935 assert_eq!(state.max_offset_for_scrollbar().height, px(60.));
1936 }
1937
1938 #[gpui::test]
1939 fn test_remeasure_item_preserves_scroll_offset(cx: &mut TestAppContext) {
1940 use crate::{
1941 AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
1942 list, point, px, size,
1943 };
1944 use std::{cell::Cell, rc::Rc};
1945
1946 let cx = cx.add_empty_window();
1947 let item_height = Rc::new(Cell::new(100usize));
1948 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
1949
1950 struct TestView {
1951 state: ListState,
1952 item_height: Rc<Cell<usize>>,
1953 }
1954
1955 impl Render for TestView {
1956 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1957 let height = self.item_height.get();
1958 list(self.state.clone(), move |index, _, _| {
1959 let height = if index == 5 { height } else { 100 };
1960 div().h(px(height as f32)).w_full().into_any()
1961 })
1962 .w_full()
1963 .h_full()
1964 }
1965 }
1966
1967 let view = cx.update(|_, cx| {
1968 cx.new(|_| TestView {
1969 state: state.clone(),
1970 item_height: item_height.clone(),
1971 })
1972 });
1973
1974 state.scroll_to(gpui::ListOffset {
1975 item_ix: 5,
1976 offset_in_item: px(40.),
1977 });
1978
1979 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1980 view.clone().into_any_element()
1981 });
1982
1983 item_height.set(200);
1984 state.remeasure_items(5..6);
1985
1986 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
1987 view.into_any_element()
1988 });
1989
1990 let offset = state.logical_scroll_top();
1991 assert_eq!(offset.item_ix, 5);
1992 assert_eq!(offset.offset_in_item, px(40.));
1993 }
1994
1995 #[gpui::test]
1996 fn test_remeasure_then_scroll_does_not_revert_scroll_position(cx: &mut TestAppContext) {
1997 use crate::{
1998 AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
1999 list, point, px, size,
2000 };
2001
2002 let cx = cx.add_empty_window();
2003 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2004
2005 struct TestView(ListState);
2006 impl Render for TestView {
2007 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2008 list(self.0.clone(), |_, _, _| {
2009 div().h(px(100.)).w_full().into_any()
2010 })
2011 .w_full()
2012 .h_full()
2013 }
2014 }
2015
2016 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2017
2018 state.scroll_to(gpui::ListOffset {
2019 item_ix: 5,
2020 offset_in_item: px(40.),
2021 });
2022
2023 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2024 view.clone().into_any_element()
2025 });
2026
2027 state.remeasure_items(5..6);
2028
2029 cx.simulate_event(ScrollWheelEvent {
2030 position: point(px(50.), px(100.)),
2031 delta: ScrollDelta::Pixels(point(px(0.), px(-30.))),
2032 ..Default::default()
2033 });
2034
2035 let offset = state.logical_scroll_top();
2036 assert_eq!(offset.item_ix, 5);
2037 assert_eq!(offset.offset_in_item, px(70.));
2038
2039 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2040 view.into_any_element()
2041 });
2042
2043 let offset = state.logical_scroll_top();
2044 assert_eq!(offset.item_ix, 5);
2045 assert_eq!(
2046 offset.offset_in_item,
2047 px(70.),
2048 "scrolling after a remeasure should not be reverted by the stale pending scroll"
2049 );
2050 }
2051
2052 #[gpui::test]
2053 fn test_scroll_after_remeasure_clamps_to_shrunk_item_height(cx: &mut TestAppContext) {
2054 use crate::{
2055 AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
2056 list, point, px, size,
2057 };
2058 use std::{cell::Cell, rc::Rc};
2059
2060 let cx = cx.add_empty_window();
2061 let item_height = Rc::new(Cell::new(100usize));
2062 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2063
2064 struct TestView {
2065 state: ListState,
2066 item_height: Rc<Cell<usize>>,
2067 }
2068
2069 impl Render for TestView {
2070 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2071 let height = self.item_height.get();
2072 list(self.state.clone(), move |index, _, _| {
2073 let height = if index == 5 { height } else { 100 };
2074 div().h(px(height as f32)).w_full().into_any()
2075 })
2076 .w_full()
2077 .h_full()
2078 }
2079 }
2080
2081 let view = cx.update(|_, cx| {
2082 cx.new(|_| TestView {
2083 state: state.clone(),
2084 item_height: item_height.clone(),
2085 })
2086 });
2087
2088 state.scroll_to(gpui::ListOffset {
2089 item_ix: 5,
2090 offset_in_item: px(40.),
2091 });
2092
2093 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2094 view.clone().into_any_element()
2095 });
2096
2097 item_height.set(50);
2098 state.remeasure_items(5..6);
2099
2100 cx.simulate_event(ScrollWheelEvent {
2101 position: point(px(50.), px(100.)),
2102 delta: ScrollDelta::Pixels(point(px(0.), px(-30.))),
2103 ..Default::default()
2104 });
2105
2106 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2107 view.into_any_element()
2108 });
2109
2110 let offset = state.logical_scroll_top();
2111 assert_eq!(offset.item_ix, 5);
2112 assert_eq!(offset.offset_in_item, px(50.));
2113 }
2114
2115 #[gpui::test]
2116 fn test_scrollbar_drag_with_growing_content(cx: &mut TestAppContext) {
2117 use crate::{
2118 AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
2119 list, point, px, size,
2120 };
2121 use std::{cell::Cell, rc::Rc};
2122
2123 let cx = cx.add_empty_window();
2124 let last_item_height = Rc::new(Cell::new(50usize));
2125 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
2126
2127 struct TestView {
2128 state: ListState,
2129 last_item_height: Rc<Cell<usize>>,
2130 }
2131
2132 impl Render for TestView {
2133 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2134 let last_item_height = self.last_item_height.clone();
2135 list(self.state.clone(), move |index, _, _| {
2136 let height = if index == 9 {
2137 last_item_height.get()
2138 } else {
2139 50
2140 };
2141 div().h(px(height as f32)).w_full().into_any()
2142 })
2143 .w_full()
2144 .h_full()
2145 }
2146 }
2147
2148 let view = cx.update(|_, cx| {
2149 cx.new(|_| TestView {
2150 state: state.clone(),
2151 last_item_height: last_item_height.clone(),
2152 })
2153 });
2154
2155 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2156 view.clone().into_any_element()
2157 });
2158
2159 state.scrollbar_drag_started();
2160 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
2161 let scrollbar_offset_before_growth = state.scroll_px_offset_for_scrollbar();
2162
2163 let offset = state.logical_scroll_top();
2164 assert_eq!(offset.item_ix, 3);
2165 assert_eq!(offset.offset_in_item, px(0.));
2166
2167 last_item_height.set(550);
2168 state.remeasure_items(9..10);
2169 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2170 view.clone().into_any_element()
2171 });
2172
2173 assert_eq!(state.max_offset_for_scrollbar().height, px(300.));
2174 assert_eq!(
2175 state.scroll_px_offset_for_scrollbar(),
2176 scrollbar_offset_before_growth
2177 );
2178
2179 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
2180 let offset = state.logical_scroll_top();
2181 assert_eq!(offset.item_ix, 3);
2182 assert_eq!(offset.offset_in_item, px(0.));
2183 }
2184
2185 #[gpui::test]
2186 fn test_follow_tail_reengages_after_scrollbar_drag_to_bottom_while_growing(
2187 cx: &mut TestAppContext,
2188 ) {
2189 use crate::{
2190 AppContext, Context, Element, FollowMode, IntoElement, ListState, Render, Styled,
2191 Window, div, list, point, px, size,
2192 };
2193
2194 let cx = cx.add_empty_window();
2195 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
2196
2197 struct TestView(ListState);
2198 impl Render for TestView {
2199 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2200 list(self.0.clone(), |_, _, _| {
2201 div().h(px(50.)).w_full().into_any()
2202 })
2203 .w_full()
2204 .h_full()
2205 }
2206 }
2207
2208 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2209
2210 state.set_follow_mode(FollowMode::Tail);
2211 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2212 view.clone().into_any_element()
2213 });
2214 assert!(state.is_following_tail());
2215
2216 state.scrollbar_drag_started();
2217
2218 state.splice(10..10, 10);
2219 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2220 view.clone().into_any_element()
2221 });
2222
2223 state.set_offset_from_scrollbar(point(px(0.), px(-300.)));
2224 state.scrollbar_drag_ended();
2225
2226 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2227 view.into_any_element()
2228 });
2229
2230 assert!(state.is_following_tail());
2231 }
2232}