1use crate::{
11 Along as _, AnyElement, App, AvailableSpace, Axis, Bounds, ContentMask, DispatchPhase, Edges,
12 Element, EntityId, FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId,
13 IntoElement, Overflow, Pixels, Point, ScrollDelta, ScrollEvent, Size, Style, StyleRefinement,
14 Styled, 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 scroll_anchor: Option<ScrollAnchor>,
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 item_measurement_ready: Option<Box<dyn Fn(usize, &App) -> bool>>,
75 follow_state: FollowState,
76}
77
78#[derive(Clone, Copy)]
79struct ScrollAnchor {
80 position: ListOffset,
81 pending: Option<PendingScroll>,
82 provisional: bool,
83 pending_end: bool,
84}
85
86#[derive(Clone, Copy)]
87enum PendingScroll {
88 Absolute(Pixels),
89 Proportional(f32),
90}
91
92enum ScrollAnchorMode {
93 Absolute,
94 Proportional,
95}
96
97#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
99pub enum FollowMode {
100 #[default]
102 Normal,
103 Tail,
105}
106
107#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
108enum FollowState {
109 #[default]
110 Normal,
111 Tail {
112 is_following: bool,
113 },
114}
115
116impl FollowState {
117 fn is_following(&self) -> bool {
118 matches!(self, FollowState::Tail { is_following: true })
119 }
120
121 fn has_stopped_following(&self) -> bool {
122 matches!(
123 self,
124 FollowState::Tail {
125 is_following: false
126 }
127 )
128 }
129
130 fn start_following(&mut self) {
131 if let FollowState::Tail {
132 is_following: false,
133 } = self
134 {
135 *self = FollowState::Tail { is_following: true };
136 }
137 }
138
139 fn stop_following(&mut self) {
140 if let FollowState::Tail { is_following: true } = self {
141 *self = FollowState::Tail {
142 is_following: false,
143 };
144 }
145 }
146}
147
148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150pub enum ListAlignment {
151 Top,
153 Bottom,
155}
156
157pub struct ListScrollEvent {
159 pub visible_range: Range<usize>,
161
162 pub count: usize,
164
165 pub is_scrolled: bool,
167
168 pub is_following_tail: bool,
170}
171
172#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
174pub enum ListSizingBehavior {
175 Infer,
177 #[default]
179 Auto,
180}
181
182#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
184pub enum ListMeasuringBehavior {
185 Measure(bool),
188 #[default]
190 Visible,
191}
192
193impl ListMeasuringBehavior {
194 fn reset(&mut self) {
195 match self {
196 ListMeasuringBehavior::Measure(has_measured) => *has_measured = false,
197 ListMeasuringBehavior::Visible => {}
198 }
199 }
200}
201
202#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
204pub enum ListHorizontalSizingBehavior {
205 #[default]
207 FitList,
208 Unconstrained,
210}
211
212struct LayoutItemsResponse {
213 max_item_width: Pixels,
214 scroll_top: ListOffset,
215 item_layouts: VecDeque<ItemLayout>,
216}
217
218struct ItemLayout {
219 index: usize,
220 element: AnyElement,
221 size: Size<Pixels>,
222}
223
224pub struct ListPrepaintState {
226 hitbox: Hitbox,
227 layout: LayoutItemsResponse,
228}
229
230#[derive(Clone)]
231enum ListItem {
232 Unmeasured {
233 size_hint: Option<Size<Pixels>>,
234 focus_handle: Option<FocusHandle>,
235 },
236 Measured {
237 size: Size<Pixels>,
238 focus_handle: Option<FocusHandle>,
239 },
240}
241
242impl ListItem {
243 fn with_measurement(&self, size: Size<Pixels>, ready: bool) -> Self {
244 if ready {
245 Self::Measured {
246 size,
247 focus_handle: self.focus_handle(),
248 }
249 } else {
250 Self::Unmeasured {
251 size_hint: Some(size),
252 focus_handle: self.focus_handle(),
253 }
254 }
255 }
256
257 fn size(&self) -> Option<Size<Pixels>> {
258 if let ListItem::Measured { size, .. } = self {
259 Some(*size)
260 } else {
261 None
262 }
263 }
264
265 fn size_hint(&self) -> Option<Size<Pixels>> {
266 match self {
267 ListItem::Measured { size, .. } => Some(*size),
268 ListItem::Unmeasured { size_hint, .. } => *size_hint,
269 }
270 }
271
272 fn focus_handle(&self) -> Option<FocusHandle> {
273 match self {
274 ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => {
275 focus_handle.clone()
276 }
277 }
278 }
279
280 fn contains_focused(&self, window: &Window, cx: &App) -> bool {
281 match self {
282 ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => {
283 focus_handle
284 .as_ref()
285 .is_some_and(|handle| handle.contains_focused(window, cx))
286 }
287 }
288 }
289}
290
291#[derive(Clone, Debug, Default, PartialEq)]
292struct ListItemSummary {
293 count: usize,
294 rendered_count: usize,
295 unrendered_count: usize,
296 height: Pixels,
297 has_focus_handles: bool,
298 has_unknown_height: bool,
299}
300
301#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
302struct Count(usize);
303
304#[derive(Clone, Debug, Default)]
305struct Height(Pixels);
306
307impl ListState {
308 pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self {
315 let state = Self(Rc::new(RefCell::new(StateInner {
316 last_layout_bounds: None,
317 last_padding: None,
318 items: SumTree::default(),
319 scroll_anchor: None,
320 alignment,
321 overdraw,
322 scroll_handler: None,
323 reset: false,
324 scrollbar_drag_start_height: None,
325 measuring_behavior: ListMeasuringBehavior::default(),
326 item_measurement_ready: None,
327 follow_state: FollowState::default(),
328 })));
329 state.splice(0..0, item_count);
330 state
331 }
332
333 pub fn measure_all(self) -> Self {
337 self.0.borrow_mut().measuring_behavior = ListMeasuringBehavior::Measure(false);
338 self
339 }
340
341 pub fn with_uniform_item_height(self, height: Pixels) -> Self {
348 self.apply_uniform_item_height(height);
349 self
350 }
351
352 pub fn reset(&self, element_count: usize) {
356 let old_count = {
357 let state = &mut *self.0.borrow_mut();
358 state.reset = true;
359 state.measuring_behavior.reset();
360 state.set_scroll_anchor(None);
361 state.scrollbar_drag_start_height = None;
362 state.items.summary().count
363 };
364
365 self.splice(0..old_count, element_count);
366 }
367
368 pub fn reset_with_uniform_height(&self, element_count: usize, height: Pixels) {
372 self.reset(element_count);
373 self.apply_uniform_item_height(height);
374 }
375
376 fn apply_uniform_item_height(&self, height: Pixels) {
377 let size_hint = Size {
378 width: px(0.),
379 height,
380 };
381 let mut state = self.0.borrow_mut();
382 let new_items = state
383 .items
384 .iter()
385 .map(|item| ListItem::Unmeasured {
386 size_hint: Some(item.size_hint().unwrap_or(size_hint)),
387 focus_handle: item.focus_handle(),
388 })
389 .collect::<Vec<_>>();
390 let mut tree = SumTree::default();
391 tree.extend(new_items, ());
392 state.items = tree;
393 }
394
395 pub fn remeasure(&self) {
400 let count = self.item_count();
401 self.remeasure_items_with_scroll_anchor(0..count, ScrollAnchorMode::Proportional);
402 }
403
404 pub fn remeasure_items(&self, range: Range<usize>) {
412 self.remeasure_items_with_scroll_anchor(range, ScrollAnchorMode::Absolute);
413 }
414
415 fn remeasure_items_with_scroll_anchor(&self, range: Range<usize>, mode: ScrollAnchorMode) {
416 let state = &mut *self.0.borrow_mut();
417
418 if let Some(anchor) = state.scroll_anchor.as_mut()
419 && range.contains(&anchor.position.item_ix)
420 {
421 anchor.pending = match mode {
422 ScrollAnchorMode::Absolute => {
423 Some(PendingScroll::Absolute(anchor.position.offset_in_item))
424 }
425 ScrollAnchorMode::Proportional => {
426 let mut cursor = state.items.cursor::<Count>(());
427 cursor.seek(&Count(anchor.position.item_ix), Bias::Right);
428 cursor
429 .item()
430 .and_then(|item| item.size())
431 .map(|size| {
432 let fraction = if size.height > px(0.) {
433 (anchor.position.offset_in_item / size.height).clamp(0., 1.)
434 } else {
435 0.
436 };
437 PendingScroll::Proportional(fraction)
438 })
439 .or(anchor.pending)
440 }
441 };
442 }
443
444 let new_items = {
447 let mut cursor = state.items.cursor::<Count>(());
448 let mut new_items = cursor.slice(&Count(range.start), Bias::Right);
449 let invalidated = cursor.slice(&Count(range.end), Bias::Right);
450 new_items.extend(
451 invalidated.iter().map(|item| ListItem::Unmeasured {
452 size_hint: item.size_hint(),
453 focus_handle: item.focus_handle(),
454 }),
455 (),
456 );
457 new_items.append(cursor.suffix(), ());
458 new_items
459 };
460 state.items = new_items;
461 state.measuring_behavior.reset();
462 }
463
464 pub fn item_count(&self) -> usize {
466 self.0.borrow().items.summary().count
467 }
468
469 pub fn is_scrolled_to_end(&self) -> Option<bool> {
472 let state = self.0.borrow();
473 let bounds = state.last_layout_bounds?;
474 let summary = state.items.summary();
475 if summary.has_unknown_height {
476 return None;
477 }
478 let padding = state.last_padding.unwrap_or_default();
479 let content_height = summary.height + padding.top + padding.bottom;
480 let scroll_max = (content_height - bounds.size.height).max(px(0.));
481 if scroll_max <= px(0.) {
482 return None;
483 }
484 let scroll_top = state.scroll_top(&state.logical_scroll_top());
485 Some(scroll_top >= scroll_max)
486 }
487
488 pub fn splice(&self, old_range: Range<usize>, count: usize) {
491 self.splice_focusable(old_range, (0..count).map(|_| None))
492 }
493
494 pub fn splice_focusable(
499 &self,
500 old_range: Range<usize>,
501 focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
502 ) {
503 let state = &mut *self.0.borrow_mut();
504
505 let mut old_items = state.items.cursor::<Count>(());
506 let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right);
507 old_items.seek_forward(&Count(old_range.end), Bias::Right);
508
509 let mut spliced_count = 0;
510 new_items.extend(
511 focus_handles.into_iter().map(|focus_handle| {
512 spliced_count += 1;
513 ListItem::Unmeasured {
514 size_hint: None,
515 focus_handle,
516 }
517 }),
518 (),
519 );
520 new_items.append(old_items.suffix(), ());
521 drop(old_items);
522 state.items = new_items;
523
524 if let Some(anchor) = state.scroll_anchor.as_mut() {
525 if anchor.provisional && old_range.start <= anchor.position.item_ix {
526 anchor.provisional = false;
527 anchor.pending = None;
528 }
529 if old_range.contains(&anchor.position.item_ix) {
530 anchor.position = ListOffset {
531 item_ix: old_range.start,
532 offset_in_item: px(0.),
533 };
534 anchor.pending = None;
535 } else if old_range.end <= anchor.position.item_ix {
536 anchor.position.item_ix = anchor.position.item_ix - old_range.len() + spliced_count;
537 }
538 }
539 }
540
541 pub fn set_scroll_handler(
543 &self,
544 handler: impl FnMut(&ListScrollEvent, &mut Window, &mut App) + 'static,
545 ) {
546 self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
547 }
548
549 pub fn logical_scroll_top(&self) -> ListOffset {
551 self.0.borrow().logical_scroll_top()
552 }
553
554 pub fn scroll_by(&self, distance: Pixels) {
556 if distance == px(0.) {
557 return;
558 }
559
560 let current_offset = self.logical_scroll_top();
561 let state = &mut *self.0.borrow_mut();
562
563 if distance < px(0.) {
564 state.follow_state.stop_following();
565 }
566
567 let mut cursor = state.items.cursor::<ListItemSummary>(());
568 cursor.seek(&Count(current_offset.item_ix), Bias::Right);
569
570 let start_pixel_offset = cursor.start().height + current_offset.offset_in_item;
571 let new_pixel_offset = (start_pixel_offset + distance).max(px(0.));
572 if new_pixel_offset > start_pixel_offset {
573 cursor.seek_forward(&Height(new_pixel_offset), Bias::Right);
574 } else {
575 cursor.seek(&Height(new_pixel_offset), Bias::Right);
576 }
577
578 let scroll_top = ListOffset {
579 item_ix: cursor.start().count,
580 offset_in_item: new_pixel_offset - cursor.start().height,
581 };
582 drop(cursor);
583 state.set_scroll_anchor(Some(scroll_top));
584 }
585
586 pub fn scroll_to_end(&self) {
593 let state = &mut *self.0.borrow_mut();
594 let item_count = state.items.summary().count;
595 state.set_scroll_anchor(Some(ListOffset {
596 item_ix: item_count,
597 offset_in_item: px(0.),
598 }));
599 }
600
601 pub fn set_follow_mode(&self, mode: FollowMode) {
606 if mode == FollowMode::Normal {
607 self.pause_following_tail();
608 }
609 let state = &mut *self.0.borrow_mut();
610
611 match mode {
612 FollowMode::Normal => {
613 state.follow_state = FollowState::Normal;
614 }
615 FollowMode::Tail => {
616 state.follow_state = FollowState::Tail { is_following: true };
617 if matches!(mode, FollowMode::Tail) {
618 let item_count = state.items.summary().count;
619 state.set_scroll_anchor(Some(ListOffset {
620 item_ix: item_count,
621 offset_in_item: px(0.),
622 }));
623 }
624 }
625 }
626 }
627
628 pub fn pause_following_tail(&self) {
638 let state = &mut *self.0.borrow_mut();
639 if state.follow_state.is_following()
640 && let Some(anchor) = state.scroll_anchor.as_mut()
641 {
642 anchor.pending_end = false;
643 }
644 state.follow_state.stop_following();
645 }
646
647 pub fn set_item_measurement_readiness(&self, ready: impl Fn(usize, &App) -> bool + 'static) {
651 self.0.borrow_mut().item_measurement_ready = Some(Box::new(ready));
652 }
653
654 pub fn is_following_tail(&self) -> bool {
657 matches!(
658 self.0.borrow().follow_state,
659 FollowState::Tail { is_following: true }
660 )
661 }
662
663 pub fn scroll_to(&self, mut scroll_top: ListOffset) {
665 let state = &mut *self.0.borrow_mut();
666 let item_count = state.items.summary().count;
667 if scroll_top.item_ix >= item_count {
668 scroll_top.item_ix = item_count;
669 scroll_top.offset_in_item = px(0.);
670 }
671
672 if scroll_top.item_ix < item_count {
673 state.follow_state.stop_following();
674 }
675
676 state.set_scroll_anchor(Some(scroll_top));
677 }
678
679 pub fn scroll_to_reveal_item(&self, ix: usize) {
681 let state = &mut *self.0.borrow_mut();
682
683 let mut scroll_top = state.logical_scroll_top();
684 let height = state
685 .last_layout_bounds
686 .map_or(px(0.), |bounds| bounds.size.height);
687 let padding = state.last_padding.unwrap_or_default();
688
689 if ix <= scroll_top.item_ix {
690 scroll_top.item_ix = ix;
691 scroll_top.offset_in_item = px(0.);
692 } else {
693 let mut cursor = state.items.cursor::<ListItemSummary>(());
694 cursor.seek(&Count(ix + 1), Bias::Right);
695 let bottom = cursor.start().height + padding.top;
696 let goal_top = px(0.).max(bottom - height + padding.bottom);
697
698 cursor.seek(&Height(goal_top), Bias::Left);
699 let start_ix = cursor.start().count;
700 let start_item_top = cursor.start().height;
701
702 if start_ix >= scroll_top.item_ix {
703 scroll_top.item_ix = start_ix;
704 scroll_top.offset_in_item = goal_top - start_item_top;
705 }
706 }
707
708 state.set_scroll_anchor(Some(scroll_top));
709 }
710
711 pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
714 let state = &*self.0.borrow();
715
716 let bounds = state.last_layout_bounds.unwrap_or_default();
717 let padding = state.last_padding.unwrap_or_default();
718 let scroll_top = state.logical_scroll_top();
719 if ix < scroll_top.item_ix {
720 return None;
721 }
722
723 let mut cursor = state.items.cursor::<Dimensions<Count, Height>>(());
724 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
725
726 let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item;
727
728 cursor.seek_forward(&Count(ix), Bias::Right);
729 if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
730 let &Dimensions(Count(count), Height(top), _) = cursor.start();
731 if count == ix {
732 let top = bounds.top() + top - scroll_top;
733 return Some(Bounds::from_corners(
734 point(bounds.left() + padding.left, top),
735 point(bounds.right() - padding.right, top + size.height),
736 ));
737 }
738 }
739 None
740 }
741
742 pub fn scrollbar_drag_started(&self) {
747 let mut state = self.0.borrow_mut();
748 state.scrollbar_drag_start_height = Some(state.items.summary().height);
749 }
750
751 pub fn scrollbar_drag_ended(&self) {
755 self.0.borrow_mut().scrollbar_drag_start_height.take();
756 }
757
758 pub fn is_scrollbar_dragging(&self) -> bool {
765 self.0.borrow().scrollbar_drag_start_height.is_some()
766 }
767
768 pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
770 self.0.borrow_mut().set_offset_from_scrollbar(point);
771 }
772
773 pub fn max_offset_for_scrollbar(&self) -> Point<Pixels> {
776 let state = self.0.borrow();
777 point(Pixels::ZERO, state.max_scroll_offset())
778 }
779
780 pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
785 let state = &self.0.borrow();
786
787 if state.scroll_anchor.is_none() && state.alignment == ListAlignment::Bottom {
788 return Point::new(px(0.), -state.max_scroll_offset());
789 }
790
791 let logical_scroll_top = state.logical_scroll_top();
792
793 let mut cursor = state.items.cursor::<ListItemSummary>(());
794 let summary: ListItemSummary =
795 cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right);
796 let offset = summary.height + logical_scroll_top.offset_in_item;
797
798 Point::new(px(0.), -offset)
799 }
800
801 pub fn viewport_bounds(&self) -> Bounds<Pixels> {
803 self.0.borrow().last_layout_bounds.unwrap_or_default()
804 }
805
806 pub fn item_is_above_viewport(&self, ix: usize) -> Option<bool> {
814 let viewport_bounds = self.0.borrow().last_layout_bounds?;
815
816 let scroll_top = self.logical_scroll_top();
817 if ix < scroll_top.item_ix {
818 return Some(true);
821 }
822
823 let item_bounds = self.bounds_for_item(ix)?;
824 Some(item_bounds.bottom() <= viewport_bounds.top())
825 }
826
827 pub fn item_is_below_viewport(&self, ix: usize) -> Option<bool> {
833 let viewport_bounds = self.0.borrow().last_layout_bounds?;
834
835 let scroll_top = self.logical_scroll_top();
836 if ix < scroll_top.item_ix {
837 return Some(false);
840 }
841
842 let item_bounds = self.bounds_for_item(ix)?;
843 Some(item_bounds.top() >= viewport_bounds.bottom())
844 }
845}
846
847impl StateInner {
848 fn max_scroll_offset(&self) -> Pixels {
849 let bounds = self.last_layout_bounds.unwrap_or_default();
850 let height = self
851 .scrollbar_drag_start_height
852 .unwrap_or_else(|| self.items.summary().height);
853 (height - bounds.size.height).max(px(0.))
854 }
855
856 fn visible_range(
857 items: &SumTree<ListItem>,
858 height: Pixels,
859 scroll_top: &ListOffset,
860 ) -> Range<usize> {
861 let mut cursor = items.cursor::<ListItemSummary>(());
862 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
863 let start_y = cursor.start().height + scroll_top.offset_in_item;
864 cursor.seek_forward(&Height(start_y + height), Bias::Left);
865 scroll_top.item_ix..cursor.start().count + 1
866 }
867
868 fn scroll(
869 &mut self,
870 scroll_top: &ListOffset,
871 height: Pixels,
872 delta: Point<Pixels>,
873 current_view: EntityId,
874 window: &mut Window,
875 cx: &mut App,
876 ) {
877 if self.reset || delta.y == px(0.) {
880 return;
881 }
882
883 let padding = self.last_padding.unwrap_or_default();
884 let scroll_max =
885 (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
886 let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
887 .max(px(0.))
888 .min(scroll_max);
889
890 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
891 self.set_scroll_anchor(None);
892 } else {
893 let (start, ..) =
894 self.items
895 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
896 let scroll_top = ListOffset {
897 item_ix: start.count,
898 offset_in_item: new_scroll_top - start.height,
899 };
900 self.set_scroll_anchor(Some(scroll_top));
901 }
902
903 if delta.y > px(0.) {
904 self.follow_state.stop_following();
905 }
906
907 if let Some(handler) = self.scroll_handler.as_mut() {
908 let visible_range = Self::visible_range(&self.items, height, scroll_top);
909 handler(
910 &ListScrollEvent {
911 visible_range,
912 count: self.items.summary().count,
913 is_scrolled: self.scroll_anchor.is_some(),
914 is_following_tail: matches!(
915 self.follow_state,
916 FollowState::Tail { is_following: true }
917 ),
918 },
919 window,
920 cx,
921 );
922 }
923
924 cx.notify(current_view);
925 }
926
927 fn logical_scroll_top(&self) -> ListOffset {
928 self.scroll_anchor
929 .map(|anchor| anchor.position)
930 .unwrap_or_else(|| match self.alignment {
931 ListAlignment::Top => ListOffset {
932 item_ix: 0,
933 offset_in_item: px(0.),
934 },
935 ListAlignment::Bottom => ListOffset {
936 item_ix: self.items.summary().count,
937 offset_in_item: px(0.),
938 },
939 })
940 }
941
942 fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
943 let (start, ..) = self.items.find::<ListItemSummary, _>(
944 (),
945 &Count(logical_scroll_top.item_ix),
946 Bias::Right,
947 );
948 start.height + logical_scroll_top.offset_in_item
949 }
950
951 fn is_item_ready(&self, index: usize, cx: &App) -> bool {
952 self.item_measurement_ready
953 .as_ref()
954 .is_none_or(|ready| ready(index, cx))
955 }
956
957 fn is_provisional(&self) -> bool {
958 self.scroll_anchor.is_some_and(|anchor| anchor.provisional)
959 }
960
961 fn set_scroll_anchor(&mut self, position: Option<ListOffset>) {
962 let Some(position) = position else {
963 self.scroll_anchor = None;
964 return;
965 };
966 let pending = self
967 .scroll_anchor
968 .and_then(|anchor| anchor.pending)
969 .filter(|_| position.item_ix < self.items.summary().count)
970 .and_then(|pending| match (pending, self.is_provisional()) {
971 (PendingScroll::Absolute(_), _) | (_, true) => {
972 Some(PendingScroll::Absolute(position.offset_in_item))
973 }
974 (PendingScroll::Proportional(_), false) => {
975 let mut cursor = self.items.cursor::<Count>(());
976 cursor.seek(&Count(position.item_ix), Bias::Right);
977 cursor
978 .item()
979 .and_then(|item| item.size_hint())
980 .filter(|size| size.height > px(0.))
981 .map(|size| {
982 PendingScroll::Proportional(
983 (position.offset_in_item / size.height).clamp(0., 1.),
984 )
985 })
986 }
987 });
988 self.scroll_anchor = Some(ScrollAnchor {
989 position,
990 pending,
991 provisional: false,
992 pending_end: false,
993 });
994 }
995
996 fn layout_all_items(
997 &mut self,
998 available_width: Pixels,
999 render_item: &mut RenderItemFn,
1000 window: &mut Window,
1001 cx: &mut App,
1002 ) {
1003 match &mut self.measuring_behavior {
1004 ListMeasuringBehavior::Visible => {
1005 return;
1006 }
1007 ListMeasuringBehavior::Measure(has_measured) => {
1008 if *has_measured {
1009 return;
1010 }
1011 *has_measured = true;
1012 }
1013 }
1014
1015 let cursor = self.items.cursor::<Count>(());
1016 let available_item_space = size(
1017 AvailableSpace::Definite(available_width),
1018 AvailableSpace::MinContent,
1019 );
1020
1021 let mut measured_items = Vec::default();
1022
1023 for (ix, item) in cursor.enumerate() {
1024 let size = item.size().unwrap_or_else(|| {
1025 let mut element = render_item(ix, window, cx);
1026 element.layout_as_root(available_item_space, window, cx)
1027 });
1028
1029 measured_items.push(item.with_measurement(size, self.is_item_ready(ix, cx)));
1030 }
1031
1032 self.items = SumTree::from_iter(measured_items, ());
1033 self.measuring_behavior =
1034 ListMeasuringBehavior::Measure(self.items.summary().unrendered_count == 0);
1035 }
1036
1037 fn layout_items(
1038 &mut self,
1039 available_width: Option<Pixels>,
1040 available_height: Pixels,
1041 padding: &Edges<Pixels>,
1042 render_item: &mut RenderItemFn,
1043 window: &mut Window,
1044 cx: &mut App,
1045 ) -> LayoutItemsResponse {
1046 let old_items = self.items.clone();
1047 let mut measured_items = VecDeque::new();
1048 let mut item_layouts = VecDeque::new();
1049 let mut rendered_height = padding.top;
1050 let mut max_item_width = px(0.);
1051 let mut scroll_top = self.logical_scroll_top();
1052
1053 if self.follow_state.is_following()
1054 || self.scroll_anchor.is_some_and(|anchor| anchor.pending_end)
1055 {
1056 scroll_top = ListOffset {
1057 item_ix: self.items.summary().count,
1058 offset_in_item: px(0.),
1059 };
1060 self.set_scroll_anchor(Some(scroll_top));
1061 }
1062
1063 let preserve_anchor = scroll_top.item_ix < self.items.summary().count;
1064 if !preserve_anchor {
1065 self.follow_state.start_following();
1066 }
1067 let mut provisional = false;
1068 let mut rendered_focused_item = false;
1069
1070 let available_item_space = size(
1071 available_width.map_or(AvailableSpace::MaxContent, |width| {
1072 AvailableSpace::Definite(width)
1073 }),
1074 AvailableSpace::MinContent,
1075 );
1076
1077 let mut cursor = old_items.cursor::<Count>(());
1078
1079 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
1081 for (ix, item) in cursor.by_ref().enumerate() {
1082 let visible_height = rendered_height - scroll_top.offset_in_item;
1083 if visible_height >= available_height + self.overdraw {
1084 break;
1085 }
1086
1087 let item_index = scroll_top.item_ix + ix;
1088 let (size, ready) = if visible_height >= available_height
1089 && let Some(size) = item.size()
1090 {
1091 (size, self.is_item_ready(item_index, cx))
1092 } else {
1093 let mut element = render_item(item_index, window, cx);
1094 let element_size = element.layout_as_root(available_item_space, window, cx);
1095 let ready = self.is_item_ready(item_index, cx);
1096 if ix == 0
1097 && ready
1098 && let Some(anchor) = self.scroll_anchor.as_mut()
1099 && let Some(pending) = anchor.pending.take()
1100 {
1101 scroll_top.offset_in_item = match pending {
1102 PendingScroll::Absolute(offset) => offset.min(element_size.height),
1103 PendingScroll::Proportional(fraction) => element_size.height * fraction,
1104 };
1105 anchor.position = scroll_top;
1106 }
1107
1108 if visible_height < available_height {
1109 item_layouts.push_back(ItemLayout {
1110 index: item_index,
1111 element,
1112 size: element_size,
1113 });
1114 if item.contains_focused(window, cx) {
1115 rendered_focused_item = true;
1116 }
1117 }
1118 (element_size, ready)
1119 };
1120
1121 rendered_height += size.height;
1122 max_item_width = max_item_width.max(size.width);
1123 provisional |= !ready;
1124 measured_items.push_back(item.with_measurement(size, ready));
1125 if !ready && visible_height >= available_height {
1126 break;
1127 }
1128 }
1129 rendered_height += padding.bottom;
1130
1131 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
1133
1134 if !provisional && rendered_height - scroll_top.offset_in_item < available_height {
1137 let forward_layout_count = item_layouts.len();
1138 let forward_focused = rendered_focused_item;
1139 while rendered_height < available_height {
1140 cursor.prev();
1141 if let Some(item) = cursor.item() {
1142 let item_index = cursor.start().0;
1143 let mut element = render_item(item_index, window, cx);
1144 let element_size = element.layout_as_root(available_item_space, window, cx);
1145 let ready = self.is_item_ready(item_index, cx);
1146 provisional |= !ready;
1147 rendered_height += element_size.height;
1148 measured_items.push_front(item.with_measurement(element_size, ready));
1149 item_layouts.push_front(ItemLayout {
1150 index: item_index,
1151 element,
1152 size: element_size,
1153 });
1154 if item.contains_focused(window, cx) {
1155 rendered_focused_item = true;
1156 }
1157 if preserve_anchor && provisional {
1158 break;
1159 }
1160 } else {
1161 break;
1162 }
1163 }
1164
1165 if preserve_anchor && provisional {
1166 item_layouts.drain(..item_layouts.len() - forward_layout_count);
1167 rendered_focused_item = forward_focused;
1168 } else {
1169 scroll_top = ListOffset {
1170 item_ix: cursor.start().0,
1171 offset_in_item: rendered_height - available_height,
1172 };
1173
1174 match self.alignment {
1175 ListAlignment::Top => {
1176 scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
1177 self.set_scroll_anchor(Some(scroll_top));
1178 }
1179 ListAlignment::Bottom => {
1180 scroll_top = ListOffset {
1181 item_ix: cursor.start().0,
1182 offset_in_item: rendered_height - available_height,
1183 };
1184 self.set_scroll_anchor(None);
1185 }
1186 };
1187 }
1188 }
1189
1190 if let Some(anchor) = self.scroll_anchor.as_mut() {
1191 anchor.provisional = preserve_anchor && provisional;
1192 anchor.pending_end = !preserve_anchor && provisional;
1193 }
1194
1195 if !provisional {
1197 let mut leading_overdraw = scroll_top.offset_in_item;
1198 while leading_overdraw < self.overdraw {
1199 cursor.prev();
1200 if let Some(item) = cursor.item() {
1201 let size = if let ListItem::Measured { size, .. } = item {
1202 *size
1203 } else {
1204 let mut element = render_item(cursor.start().0, window, cx);
1205 element.layout_as_root(available_item_space, window, cx)
1206 };
1207
1208 leading_overdraw += size.height;
1209 let ready = self.is_item_ready(cursor.start().0, cx);
1210 measured_items.push_front(item.with_measurement(size, ready));
1211 if !ready {
1212 break;
1213 }
1214 } else {
1215 break;
1216 }
1217 }
1218 }
1219
1220 let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
1221 let mut cursor = old_items.cursor::<Count>(());
1222 let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right);
1223 new_items.extend(measured_items, ());
1224 cursor.seek(&Count(measured_range.end), Bias::Right);
1225 new_items.append(cursor.suffix(), ());
1226 self.items = new_items;
1227
1228 if !provisional && self.follow_state.has_stopped_following() {
1232 let padding = self.last_padding.unwrap_or_default();
1233 let total_height = self.items.summary().height + padding.top + padding.bottom;
1234 let scroll_offset = self.scroll_top(&scroll_top);
1235 if scroll_offset + available_height >= total_height - px(1.0) {
1236 self.follow_state.start_following();
1237 }
1238 }
1239
1240 if !rendered_focused_item {
1244 let mut cursor = self
1245 .items
1246 .filter::<_, Count>((), |summary| summary.has_focus_handles);
1247 cursor.next();
1248 while let Some(item) = cursor.item() {
1249 if item.contains_focused(window, cx) {
1250 let item_index = cursor.start().0;
1251 let mut element = render_item(cursor.start().0, window, cx);
1252 let size = element.layout_as_root(available_item_space, window, cx);
1253 item_layouts.push_back(ItemLayout {
1254 index: item_index,
1255 element,
1256 size,
1257 });
1258 break;
1259 }
1260 cursor.next();
1261 }
1262 }
1263
1264 LayoutItemsResponse {
1265 max_item_width,
1266 scroll_top,
1267 item_layouts,
1268 }
1269 }
1270
1271 fn prepaint_items(
1272 &mut self,
1273 bounds: Bounds<Pixels>,
1274 padding: Edges<Pixels>,
1275 autoscroll: bool,
1276 render_item: &mut RenderItemFn,
1277 window: &mut Window,
1278 cx: &mut App,
1279 ) -> Result<LayoutItemsResponse, ListOffset> {
1280 let item_width = bounds.size.width - padding.along(Axis::Horizontal);
1281 window.transact(|window| {
1282 match self.measuring_behavior {
1283 ListMeasuringBehavior::Measure(has_measured) if !has_measured => {
1284 self.layout_all_items(item_width, render_item, window, cx);
1285 }
1286 _ => {}
1287 }
1288
1289 let mut layout_response = self.layout_items(
1290 Some(item_width),
1291 bounds.size.height,
1292 &padding,
1293 render_item,
1294 window,
1295 cx,
1296 );
1297
1298 window.take_autoscroll();
1300
1301 if bounds.size.height > padding.top + padding.bottom {
1303 let mut item_origin = bounds.origin + Point::new(padding.left, padding.top);
1304 item_origin.y -= layout_response.scroll_top.offset_in_item;
1305 for item in &mut layout_response.item_layouts {
1306 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1307 item.element.prepaint_at(item_origin, window, cx);
1308 });
1309
1310 if let Some(autoscroll_bounds) = window.take_autoscroll()
1311 && autoscroll
1312 {
1313 if autoscroll_bounds.top() < bounds.top() {
1314 let mut item_ix = item.index;
1315 let mut offset_in_item = autoscroll_bounds.top() - item_origin.y;
1316
1317 if offset_in_item < Pixels::ZERO {
1322 let mut cursor = self.items.cursor::<Count>(());
1323 cursor.seek(&Count(item_ix), Bias::Right);
1324 while offset_in_item < Pixels::ZERO {
1325 cursor.prev();
1326 let Some(prev_item) = cursor.item() else {
1327 offset_in_item = Pixels::ZERO;
1328 break;
1329 };
1330 let size = prev_item.size().unwrap_or_else(|| {
1331 let mut element = render_item(cursor.start().0, window, cx);
1332 let item_available_size =
1333 size(item_width.into(), AvailableSpace::MinContent);
1334 element.layout_as_root(item_available_size, window, cx)
1335 });
1336 item_ix = cursor.start().0;
1337 offset_in_item += size.height;
1338 }
1339 }
1340
1341 return Err(ListOffset {
1342 item_ix,
1343 offset_in_item,
1344 });
1345 } else if autoscroll_bounds.bottom() > bounds.bottom() {
1346 let mut cursor = self.items.cursor::<Count>(());
1347 cursor.seek(&Count(item.index), Bias::Right);
1348 let mut height = bounds.size.height - padding.top - padding.bottom;
1349
1350 height -= autoscroll_bounds.bottom() - item_origin.y;
1352
1353 while height > Pixels::ZERO {
1355 cursor.prev();
1356 let Some(item) = cursor.item() else { break };
1357
1358 let size = item.size().unwrap_or_else(|| {
1359 let mut item = render_item(cursor.start().0, window, cx);
1360 let item_available_size =
1361 size(item_width.into(), AvailableSpace::MinContent);
1362 item.layout_as_root(item_available_size, window, cx)
1363 });
1364 height -= size.height;
1365 }
1366
1367 return Err(ListOffset {
1368 item_ix: cursor.start().0,
1369 offset_in_item: if height < Pixels::ZERO {
1370 -height
1371 } else {
1372 Pixels::ZERO
1373 },
1374 });
1375 }
1376 }
1377
1378 item_origin.y += item.size.height;
1379 }
1380 } else {
1381 layout_response.item_layouts.clear();
1382 }
1383
1384 Ok(layout_response)
1385 })
1386 }
1387
1388 fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
1391 let Some(bounds) = self.last_layout_bounds else {
1392 return;
1393 };
1394 let height = bounds.size.height;
1395
1396 let padding = self.last_padding.unwrap_or_default();
1397 let content_height = self
1400 .scrollbar_drag_start_height
1401 .unwrap_or_else(|| self.items.summary().height);
1402 let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
1403 let new_scroll_top = (-point.y).max(px(0.)).min(scroll_max);
1404
1405 let dragged_to_end =
1408 scroll_max > px(0.) && new_scroll_top >= (scroll_max - px(1.0)).max(px(0.));
1409 if dragged_to_end && matches!(self.follow_state, FollowState::Tail { .. }) {
1410 self.follow_state = FollowState::Tail { is_following: true };
1411 let item_count = self.items.summary().count;
1412 self.set_scroll_anchor(Some(ListOffset {
1413 item_ix: item_count,
1414 offset_in_item: px(0.),
1415 }));
1416 return;
1417 }
1418
1419 self.follow_state.stop_following();
1420
1421 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
1422 self.set_scroll_anchor(None);
1423 } else {
1424 let (start, _, _) =
1425 self.items
1426 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
1427
1428 let scroll_top = ListOffset {
1429 item_ix: start.count,
1430 offset_in_item: new_scroll_top - start.height,
1431 };
1432 self.set_scroll_anchor(Some(scroll_top));
1433 }
1434 }
1435}
1436
1437impl std::fmt::Debug for ListItem {
1438 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1439 match self {
1440 Self::Unmeasured { .. } => write!(f, "Unrendered"),
1441 Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
1442 }
1443 }
1444}
1445
1446#[derive(Debug, Clone, Copy, Default, PartialEq)]
1449pub struct ListOffset {
1450 pub item_ix: usize,
1452 pub offset_in_item: Pixels,
1454}
1455
1456impl Element for List {
1457 type RequestLayoutState = ();
1458 type PrepaintState = ListPrepaintState;
1459
1460 fn id(&self) -> Option<crate::ElementId> {
1461 None
1462 }
1463
1464 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1465 None
1466 }
1467
1468 fn request_layout(
1469 &mut self,
1470 _id: Option<&GlobalElementId>,
1471 _inspector_id: Option<&InspectorElementId>,
1472 window: &mut Window,
1473 cx: &mut App,
1474 ) -> (crate::LayoutId, Self::RequestLayoutState) {
1475 let layout_id = match self.sizing_behavior {
1476 ListSizingBehavior::Infer => {
1477 let mut style = Style::default();
1478 style.overflow.y = Overflow::Scroll;
1479 style.refine(&self.style);
1480 window.with_text_style(style.text_style().cloned(), |window| {
1481 let state = &mut *self.state.0.borrow_mut();
1482
1483 let available_height = if let Some(last_bounds) = state.last_layout_bounds {
1484 last_bounds.size.height
1485 } else {
1486 state.overdraw
1489 };
1490 let padding = content_insets(
1491 &style,
1492 state.last_layout_bounds.unwrap_or_default().size,
1493 window.rem_size(),
1494 );
1495
1496 let layout_response = state.layout_items(
1497 None,
1498 available_height,
1499 &padding,
1500 &mut self.render_item,
1501 window,
1502 cx,
1503 );
1504 let max_element_width = layout_response.max_item_width;
1505
1506 let summary = state.items.summary();
1507 let total_height = summary.height;
1508
1509 window.request_measured_layout(
1510 style,
1511 move |known_dimensions, available_space, _window, _cx| {
1512 let width =
1513 known_dimensions
1514 .width
1515 .unwrap_or(match available_space.width {
1516 AvailableSpace::Definite(x) => x,
1517 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1518 max_element_width
1519 }
1520 });
1521 let height = match available_space.height {
1522 AvailableSpace::Definite(height) => total_height.min(height),
1523 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1524 total_height
1525 }
1526 };
1527 size(width, height)
1528 },
1529 )
1530 })
1531 }
1532 ListSizingBehavior::Auto => {
1533 let mut style = Style::default();
1534 style.refine(&self.style);
1535 window.with_text_style(style.text_style().cloned(), |window| {
1536 window.request_layout(style, None, cx)
1537 })
1538 }
1539 };
1540 (layout_id, ())
1541 }
1542
1543 fn prepaint(
1544 &mut self,
1545 _id: Option<&GlobalElementId>,
1546 _inspector_id: Option<&InspectorElementId>,
1547 bounds: Bounds<Pixels>,
1548 _: &mut Self::RequestLayoutState,
1549 window: &mut Window,
1550 cx: &mut App,
1551 ) -> ListPrepaintState {
1552 let state = &mut *self.state.0.borrow_mut();
1553 state.reset = false;
1554
1555 let mut style = Style::default();
1556 style.refine(&self.style);
1557
1558 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1559
1560 let padding = content_insets(&style, bounds.size, window.rem_size());
1561
1562 let last_item_width =
1565 state
1566 .last_layout_bounds
1567 .zip(state.last_padding)
1568 .map(|(last_bounds, last_padding)| {
1569 last_bounds.size.width - last_padding.along(Axis::Horizontal)
1570 });
1571 if last_item_width
1572 .is_none_or(|width| width != bounds.size.width - padding.along(Axis::Horizontal))
1573 {
1574 let new_items = SumTree::from_iter(
1575 state.items.iter().map(|item| ListItem::Unmeasured {
1576 size_hint: None,
1577 focus_handle: item.focus_handle(),
1578 }),
1579 (),
1580 );
1581
1582 state.items = new_items;
1583 state.measuring_behavior.reset();
1584 }
1585
1586 let mut autoscroll = true;
1587 let layout = loop {
1588 match state.prepaint_items(
1589 bounds,
1590 padding,
1591 autoscroll,
1592 &mut self.render_item,
1593 window,
1594 cx,
1595 ) {
1596 Ok(layout) => break layout,
1597 Err(autoscroll_request) => {
1598 state.set_scroll_anchor(Some(autoscroll_request));
1599 autoscroll = false;
1600 }
1601 }
1602 };
1603
1604 state.last_layout_bounds = Some(bounds);
1605 state.last_padding = Some(padding);
1606 ListPrepaintState { hitbox, layout }
1607 }
1608
1609 fn paint(
1610 &mut self,
1611 _id: Option<&GlobalElementId>,
1612 _inspector_id: Option<&InspectorElementId>,
1613 bounds: Bounds<crate::Pixels>,
1614 _: &mut Self::RequestLayoutState,
1615 prepaint: &mut Self::PrepaintState,
1616 window: &mut Window,
1617 cx: &mut App,
1618 ) {
1619 let current_view = window.current_view();
1620
1621 let list_state = self.state.clone();
1627 let height = bounds.size.height;
1628 let scroll_top = prepaint.layout.scroll_top;
1629 let hitbox_id = prepaint.hitbox.id;
1630 let mut accumulated_scroll_delta = ScrollDelta::default();
1631 window.on_mouse_event(move |event: &ScrollEvent, phase, window, cx| {
1632 if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) {
1633 accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
1634 let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
1635 list_state.0.borrow_mut().scroll(
1636 &scroll_top,
1637 height,
1638 pixel_delta,
1639 current_view,
1640 window,
1641 cx,
1642 )
1643 }
1644 });
1645
1646 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1647 for item in &mut prepaint.layout.item_layouts {
1648 item.element.paint(window, cx);
1649 }
1650 });
1651 }
1652}
1653
1654fn content_insets(style: &Style, size: Size<Pixels>, rem_size: Pixels) -> Edges<Pixels> {
1655 let padding = style.padding.to_pixels(size.into(), rem_size);
1656 let border = style.border_widths.to_pixels(rem_size);
1657
1658 padding + border
1659}
1660
1661impl IntoElement for List {
1662 type Element = Self;
1663
1664 fn into_element(self) -> Self::Element {
1665 self
1666 }
1667}
1668
1669impl Styled for List {
1670 fn style(&mut self) -> &mut StyleRefinement {
1671 &mut self.style
1672 }
1673}
1674
1675impl sum_tree::Item for ListItem {
1676 type Summary = ListItemSummary;
1677
1678 fn summary(&self, _: ()) -> Self::Summary {
1679 match self {
1680 ListItem::Unmeasured {
1681 size_hint,
1682 focus_handle,
1683 } => ListItemSummary {
1684 count: 1,
1685 rendered_count: 0,
1686 unrendered_count: 1,
1687 height: if let Some(size) = size_hint {
1688 size.height
1689 } else {
1690 px(0.)
1691 },
1692 has_focus_handles: focus_handle.is_some(),
1693 has_unknown_height: size_hint.is_none(),
1694 },
1695 ListItem::Measured {
1696 size, focus_handle, ..
1697 } => ListItemSummary {
1698 count: 1,
1699 rendered_count: 1,
1700 unrendered_count: 0,
1701 height: size.height,
1702 has_focus_handles: focus_handle.is_some(),
1703 has_unknown_height: false,
1704 },
1705 }
1706 }
1707}
1708
1709impl sum_tree::ContextLessSummary for ListItemSummary {
1710 fn zero() -> Self {
1711 Default::default()
1712 }
1713
1714 fn add_summary(&mut self, summary: &Self) {
1715 self.count += summary.count;
1716 self.rendered_count += summary.rendered_count;
1717 self.unrendered_count += summary.unrendered_count;
1718 self.height += summary.height;
1719 self.has_focus_handles |= summary.has_focus_handles;
1720 self.has_unknown_height |= summary.has_unknown_height;
1721 }
1722}
1723
1724impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
1725 fn zero(_cx: ()) -> Self {
1726 Default::default()
1727 }
1728
1729 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1730 self.0 += summary.count;
1731 }
1732}
1733
1734impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
1735 fn zero(_cx: ()) -> Self {
1736 Default::default()
1737 }
1738
1739 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1740 self.0 += summary.height;
1741 }
1742}
1743
1744impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Count {
1745 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1746 Ord::cmp(&self.0, &other.count)
1747 }
1748}
1749
1750impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height {
1751 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1752 Ord::cmp(&self.0, &other.height)
1753 }
1754}
1755
1756#[cfg(test)]
1757mod test {
1758
1759 use gpui::{ScrollDelta, ScrollEvent};
1760 use std::cell::{Cell, RefCell};
1761 use std::rc::Rc;
1762
1763 use crate::{
1764 self as gpui, AnyElement, AppContext, Bounds, Context, Element, FollowMode,
1765 InteractiveElement, IntoElement, ListOffset, ListSizingBehavior, ListState, ParentElement,
1766 Pixels, Render, Styled, TestAppContext, VisualTestContext, Window, canvas, div, list,
1767 point, px, size,
1768 };
1769
1770 #[gpui::test]
1771 fn test_autoscroll_above_item_top_renders_items_above(cx: &mut TestAppContext) {
1772 let cx = cx.add_empty_window();
1773
1774 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1775 state.scroll_to(gpui::ListOffset {
1776 item_ix: 2,
1777 offset_in_item: px(0.),
1778 });
1779
1780 struct TestView(ListState);
1781 impl Render for TestView {
1782 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1783 list(self.0.clone(), |ix, _, _| {
1784 if ix == 2 {
1785 canvas(
1788 |bounds, window, _| {
1789 window.request_autoscroll(Bounds::from_corners(
1790 point(bounds.left(), bounds.top() - px(30.)),
1791 point(bounds.right(), bounds.top() + px(5.)),
1792 ));
1793 },
1794 |_, _, _, _| {},
1795 )
1796 .h(px(20.))
1797 .w_full()
1798 .into_any()
1799 } else {
1800 div().h(px(20.)).w_full().into_any()
1801 }
1802 })
1803 .w_full()
1804 .h_full()
1805 }
1806 }
1807
1808 cx.draw(point(px(0.), px(0.)), size(px(100.), px(60.)), |_, cx| {
1809 cx.new(|_| TestView(state.clone())).into_any_element()
1810 });
1811
1812 let scroll_top = state.logical_scroll_top();
1814 assert!(
1815 scroll_top.offset_in_item >= px(0.),
1816 "offset_in_item must never be negative (would leave blank space above), got {:?}",
1817 scroll_top.offset_in_item,
1818 );
1819 assert_eq!(scroll_top.item_ix, 0);
1820 assert_eq!(scroll_top.offset_in_item, px(10.));
1821 }
1822
1823 #[gpui::test]
1824 fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
1825 let cx = cx.add_empty_window();
1826
1827 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1828
1829 state.scroll_to(gpui::ListOffset {
1831 item_ix: 0,
1832 offset_in_item: px(0.0),
1833 });
1834
1835 struct TestView(ListState);
1836 impl Render for TestView {
1837 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1838 list(self.0.clone(), |_, _, _| {
1839 div().h(px(10.)).w_full().into_any()
1840 })
1841 .w_full()
1842 .h_full()
1843 }
1844 }
1845
1846 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1848 cx.new(|_| TestView(state.clone())).into_any_element()
1849 });
1850
1851 state.reset(5);
1853
1854 cx.simulate_event(ScrollEvent {
1856 position: point(px(1.), px(1.)),
1857 delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
1858 ..Default::default()
1859 });
1860
1861 assert_eq!(state.logical_scroll_top().item_ix, 0);
1863 assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
1864 }
1865
1866 #[gpui::test]
1867 fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) {
1868 let cx = cx.add_empty_window();
1869
1870 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
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(100.)), |_, cx| {
1885 cx.new(|_| TestView(state.clone())).into_any_element()
1886 });
1887
1888 state.scroll_by(px(30.));
1890
1891 let offset = state.logical_scroll_top();
1893 assert_eq!(offset.item_ix, 1);
1894 assert_eq!(offset.offset_in_item, px(10.));
1895
1896 state.scroll_by(px(-30.));
1898
1899 let offset = state.logical_scroll_top();
1901 assert_eq!(offset.item_ix, 0);
1902 assert_eq!(offset.offset_in_item, px(0.));
1903
1904 state.scroll_by(px(0.));
1906 let offset = state.logical_scroll_top();
1907 assert_eq!(offset.item_ix, 0);
1908 assert_eq!(offset.offset_in_item, px(0.));
1909 }
1910
1911 #[gpui::test]
1912 fn test_child_scroll_handler_can_stop_list_scroll(cx: &mut TestAppContext) {
1913 let cx = cx.add_empty_window();
1914
1915 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1916 let child_saw_event = Rc::new(Cell::new(false));
1917
1918 struct TestView {
1919 state: ListState,
1920 child_saw_event: Rc<Cell<bool>>,
1921 }
1922 impl Render for TestView {
1923 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1924 let child_saw_event = self.child_saw_event.clone();
1925 list(self.state.clone(), move |_, _, _| {
1926 let child_saw_event = child_saw_event.clone();
1927 div()
1928 .h(px(20.))
1929 .w_full()
1930 .on_scroll(move |_, _, cx| {
1931 child_saw_event.set(true);
1932 cx.stop_propagation();
1933 })
1934 .into_any()
1935 })
1936 .w_full()
1937 .h_full()
1938 }
1939 }
1940
1941 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
1942 cx.new(|_| TestView {
1943 state: state.clone(),
1944 child_saw_event: child_saw_event.clone(),
1945 })
1946 .into_any_element()
1947 });
1948
1949 cx.simulate_event(ScrollEvent {
1950 position: point(px(50.), px(10.)),
1951 delta: ScrollDelta::Pixels(point(px(0.), px(-30.))),
1952 ..Default::default()
1953 });
1954
1955 assert!(
1956 child_saw_event.get(),
1957 "the child's scroll-wheel handler should run"
1958 );
1959 let offset = state.logical_scroll_top();
1961 assert_eq!(offset.item_ix, 0);
1962 assert_eq!(offset.offset_in_item, px(0.));
1963 }
1964
1965 #[gpui::test]
1966 fn test_item_viewport_queries_return_none_before_layout(_cx: &mut TestAppContext) {
1967 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1968
1969 assert_eq!(state.item_is_above_viewport(0), None);
1970 assert_eq!(state.item_is_below_viewport(0), None);
1971 }
1972
1973 #[gpui::test]
1974 fn test_item_viewport_queries_before_logical_scroll_top(cx: &mut TestAppContext) {
1975 let cx = cx.add_empty_window();
1976
1977 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1978
1979 state.scroll_to(gpui::ListOffset {
1980 item_ix: 2,
1981 offset_in_item: px(0.),
1982 });
1983 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1984 cx.new(|_| TestListView(state.clone())).into_any_element()
1985 });
1986
1987 assert_eq!(state.item_is_above_viewport(1), Some(true));
1988 assert_eq!(state.item_is_below_viewport(1), Some(false));
1989 }
1990
1991 #[gpui::test]
1992 fn test_item_viewport_queries_measured_item_inside_viewport(cx: &mut TestAppContext) {
1993 let cx = cx.add_empty_window();
1994
1995 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
1996
1997 state.scroll_to(gpui::ListOffset {
1998 item_ix: 2,
1999 offset_in_item: px(0.),
2000 });
2001 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2002 cx.new(|_| TestListView(state.clone())).into_any_element()
2003 });
2004
2005 assert_eq!(state.item_is_above_viewport(2), Some(false));
2006 assert_eq!(state.item_is_below_viewport(2), Some(false));
2007 }
2008
2009 #[gpui::test]
2010 fn test_item_viewport_queries_measured_item_above_viewport(cx: &mut TestAppContext) {
2011 let cx = cx.add_empty_window();
2012
2013 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2014
2015 state.scroll_to(gpui::ListOffset {
2016 item_ix: 2,
2017 offset_in_item: px(20.),
2018 });
2019 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2020 cx.new(|_| TestListView(state.clone())).into_any_element()
2021 });
2022
2023 assert_eq!(state.item_is_above_viewport(2), Some(true));
2024 assert_eq!(state.item_is_below_viewport(2), Some(false));
2025 }
2026
2027 #[gpui::test]
2028 fn test_item_viewport_queries_measured_item_below_viewport(cx: &mut TestAppContext) {
2029 let cx = cx.add_empty_window();
2030
2031 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2032
2033 state.scroll_to(gpui::ListOffset {
2034 item_ix: 2,
2035 offset_in_item: px(0.),
2036 });
2037 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2038 cx.new(|_| TestListView(state.clone())).into_any_element()
2039 });
2040
2041 assert_eq!(state.item_is_above_viewport(3), Some(false));
2042 assert_eq!(state.item_is_below_viewport(3), Some(true));
2043 }
2044
2045 #[gpui::test]
2046 fn test_item_viewport_queries_remain_stable_with_zero_height_viewport(cx: &mut TestAppContext) {
2047 let cx = cx.add_empty_window();
2048
2049 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2050
2051 state.scroll_to(gpui::ListOffset {
2052 item_ix: 2,
2053 offset_in_item: px(0.),
2054 });
2055 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2056 cx.new(|_| TestListView(state.clone())).into_any_element()
2057 });
2058
2059 assert_eq!(state.item_is_above_viewport(3), Some(false));
2060 assert_eq!(state.item_is_below_viewport(3), Some(true));
2061
2062 cx.draw(point(px(0.), px(0.)), size(px(100.), px(0.)), |_, cx| {
2067 cx.new(|_| TestListView(state.clone())).into_any_element()
2068 });
2069
2070 assert_eq!(state.item_is_above_viewport(1), Some(true));
2071 assert_eq!(state.item_is_below_viewport(1), Some(false));
2072 assert_eq!(state.item_is_above_viewport(3), Some(false));
2073 assert_eq!(state.item_is_below_viewport(3), Some(true));
2074 }
2075
2076 #[gpui::test]
2077 fn test_item_viewport_queries_after_scroll_to_end_before_layout(cx: &mut TestAppContext) {
2078 let cx = cx.add_empty_window();
2079
2080 let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
2081
2082 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
2083 cx.new(|_| TestListView(state.clone())).into_any_element()
2084 });
2085
2086 state.scroll_to_end();
2087
2088 assert_eq!(state.logical_scroll_top().item_ix, state.item_count());
2089 assert_eq!(state.item_is_above_viewport(0), Some(true));
2090 assert_eq!(state.item_is_below_viewport(0), Some(false));
2091 }
2092
2093 #[gpui::test]
2094 fn test_measure_all_after_width_change(cx: &mut TestAppContext) {
2095 let cx = cx.add_empty_window();
2096
2097 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
2098
2099 struct TestView(ListState);
2100 impl Render for TestView {
2101 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2102 list(self.0.clone(), |_, _, _| {
2103 div().h(px(50.)).w_full().into_any()
2104 })
2105 .w_full()
2106 .h_full()
2107 }
2108 }
2109
2110 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
2111
2112 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2115 view.clone().into_any_element()
2116 });
2117 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
2118
2119 cx.draw(point(px(0.), px(0.)), size(px(200.), px(200.)), |_, _| {
2123 view.into_any_element()
2124 });
2125 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
2126 }
2127
2128 #[gpui::test]
2129 fn test_remeasure(cx: &mut TestAppContext) {
2130 let cx = cx.add_empty_window();
2131
2132 let item_height = Rc::new(Cell::new(100usize));
2136 let state = ListState::new(10, crate::ListAlignment::Top, px(10.));
2137
2138 struct TestView {
2139 state: ListState,
2140 item_height: Rc<Cell<usize>>,
2141 }
2142
2143 impl Render for TestView {
2144 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2145 let height = self.item_height.get();
2146 list(self.state.clone(), move |_, _, _| {
2147 div().h(px(height as f32)).w_full().into_any()
2148 })
2149 .w_full()
2150 .h_full()
2151 }
2152 }
2153
2154 let state_clone = state.clone();
2155 let item_height_clone = item_height.clone();
2156 let view = cx.update(|_, cx| {
2157 cx.new(|_| TestView {
2158 state: state_clone,
2159 item_height: item_height_clone,
2160 })
2161 });
2162
2163 state.scroll_to(gpui::ListOffset {
2166 item_ix: 2,
2167 offset_in_item: px(40.),
2168 });
2169
2170 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2171 view.clone().into_any_element()
2172 });
2173
2174 let offset = state.logical_scroll_top();
2175 assert_eq!(offset.item_ix, 2);
2176 assert_eq!(offset.offset_in_item, px(40.));
2177
2178 item_height.set(50);
2183 state.remeasure();
2184
2185 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2186 view.into_any_element()
2187 });
2188
2189 let offset = state.logical_scroll_top();
2190 assert_eq!(offset.item_ix, 2);
2191 assert_eq!(offset.offset_in_item, px(20.));
2192 }
2193
2194 #[gpui::test]
2195 fn test_remeasure_item_preserves_scroll_offset(cx: &mut TestAppContext) {
2196 let cx = cx.add_empty_window();
2197
2198 let item_height = Rc::new(Cell::new(100usize));
2199 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2200
2201 struct TestView {
2202 state: ListState,
2203 item_height: Rc<Cell<usize>>,
2204 }
2205
2206 impl Render for TestView {
2207 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2208 let height = self.item_height.get();
2209 list(self.state.clone(), move |index, _, _| {
2210 let height = if index == 5 { height } else { 100 };
2211 div().h(px(height as f32)).w_full().into_any()
2212 })
2213 .w_full()
2214 .h_full()
2215 }
2216 }
2217
2218 let state_clone = state.clone();
2219 let item_height_clone = item_height.clone();
2220 let view = cx.update(|_, cx| {
2221 cx.new(|_| TestView {
2222 state: state_clone,
2223 item_height: item_height_clone,
2224 })
2225 });
2226
2227 state.scroll_to(gpui::ListOffset {
2228 item_ix: 5,
2229 offset_in_item: px(40.),
2230 });
2231
2232 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2233 view.clone().into_any_element()
2234 });
2235
2236 item_height.set(200);
2237 state.remeasure_items(5..6);
2238
2239 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2240 view.into_any_element()
2241 });
2242
2243 let offset = state.logical_scroll_top();
2244 assert_eq!(offset.item_ix, 5);
2245 assert_eq!(offset.offset_in_item, px(40.));
2246 }
2247
2248 #[gpui::test]
2249 fn test_provisional_item_heights_keep_scroll_anchor(cx: &mut TestAppContext) {
2250 let cx = cx.add_empty_window();
2251 let state = ListState::new(20, crate::ListAlignment::Top, px(100.));
2252 let painted = Rc::new(RefCell::new(Vec::new()));
2253 let first_rendered = Rc::new(Cell::new(None::<usize>));
2254 let ready = item_readiness(&state, false);
2255 state.set_follow_mode(FollowMode::Tail);
2256 state.scroll_to(list_offset(10, 40.));
2257
2258 for (height, measurement_ready) in [(0., false), (100., false), (100., true)] {
2259 ready.set(measurement_ready);
2260 painted.borrow_mut().clear();
2261 first_rendered.set(None);
2262 draw_list(
2263 &state,
2264 200.,
2265 {
2266 let painted = painted.clone();
2267 let first_rendered = first_rendered.clone();
2268 move |index| {
2269 first_rendered.set(Some(first_rendered.get().unwrap_or(index).min(index)));
2270 painted_item(index, height, &painted)
2271 }
2272 },
2273 cx,
2274 );
2275 assert_eq!(state.logical_scroll_top(), list_offset(10, 40.));
2276 assert!(!state.is_following_tail());
2277 assert_eq!(
2278 first_rendered.get(),
2279 Some(if measurement_ready { 9 } else { 10 })
2280 );
2281 if height > 0. {
2282 assert_eq!(
2283 painted.borrow().as_slice(),
2284 &[
2285 (10, px(-40.), px(60.)),
2286 (11, px(60.), px(160.)),
2287 (12, px(160.), px(260.)),
2288 ]
2289 );
2290 }
2291 }
2292 }
2293
2294 #[gpui::test]
2295 fn test_unready_measurements_preserve_pending_fraction(cx: &mut TestAppContext) {
2296 let cx = cx.add_empty_window();
2297 for (provisional_height, destination, final_height, expected_offset) in [
2298 (Some(200.), None, 200., 100.),
2299 (None, Some(40.), 200., 80.),
2300 (Some(200.), Some(50.), 200., 50.),
2301 (Some(0.), Some(40.), 20., 20.),
2302 (Some(10.), Some(40.), 100., 40.),
2303 (Some(200.), Some(50.), 300., 50.),
2304 ] {
2305 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2306 let painted = Rc::new(RefCell::new(Vec::new()));
2307 let draw = |height, cx: &mut VisualTestContext| {
2308 painted.borrow_mut().clear();
2309 let painted = painted.clone();
2310 draw_list(
2311 &state,
2312 200.,
2313 move |index| {
2314 painted_item(index, if index == 5 { height } else { 100. }, &painted)
2315 },
2316 cx,
2317 );
2318 };
2319 let ready = item_readiness(&state, true);
2320 state.scroll_to(list_offset(5, 50.));
2321 draw(100., cx);
2322 ready.set(false);
2323 state.remeasure();
2324 if let Some(height) = provisional_height {
2325 for height in [0., height, height] {
2326 draw(height, cx);
2327 assert_eq!(state.logical_scroll_top(), list_offset(5, 50.));
2328 }
2329 }
2330 if let Some(offset) = destination {
2331 state.scroll_to(list_offset(5, offset));
2332 }
2333 ready.set(true);
2334 for _ in 0..2 {
2335 draw(final_height, cx);
2336 assert_eq!(state.logical_scroll_top(), list_offset(5, expected_offset));
2337 assert_eq!(
2338 painted.borrow().first().copied(),
2339 Some((5, px(-expected_offset), px(final_height - expected_offset)))
2340 );
2341 }
2342 }
2343 }
2344
2345 #[gpui::test]
2346 fn test_navigation_cancels_provisional_and_paints_destination(cx: &mut TestAppContext) {
2347 let cx = cx.add_empty_window();
2348 for measured in [false, true] {
2349 let navigations: [(&str, fn(&ListState), usize, f32); 7] = [
2350 (
2351 "scroll_to",
2352 |state| state.scroll_to(list_offset(12, 0.)),
2353 12,
2354 0.,
2355 ),
2356 ("end", ListState::scroll_to_end, 19, 150.),
2357 ("down", |state| state.scroll_by(px(5000.)), 19, 150.),
2358 (
2359 "up",
2360 |state| state.scroll_by(px(-5000.)),
2361 if measured { 0 } else { 6 },
2362 0.,
2363 ),
2364 (
2365 "reveal_below",
2366 |state| state.scroll_to_reveal_item(10),
2367 10,
2368 150.,
2369 ),
2370 (
2371 "reveal_above",
2372 |state| state.scroll_to_reveal_item(0),
2373 0,
2374 0.,
2375 ),
2376 (
2377 "follow_tail",
2378 |state| state.set_follow_mode(FollowMode::Tail),
2379 19,
2380 150.,
2381 ),
2382 ];
2383 for (name, navigate, destination, top) in navigations {
2384 let state = ListState::new(20, crate::ListAlignment::Top, px(0.));
2385 let state = if measured { state.measure_all() } else { state };
2386 let painted = Rc::new(RefCell::new(Vec::new()));
2387 let draw = |cx: &mut VisualTestContext| {
2388 painted.borrow_mut().clear();
2389 let painted = painted.clone();
2390 draw_list(
2391 &state,
2392 200.,
2393 move |index| {
2394 painted_item(index, if index == 5 { 0. } else { 50. }, &painted)
2395 },
2396 cx,
2397 );
2398 };
2399 state.set_item_measurement_readiness(|index, _| index != 5);
2400 state.scroll_to(list_offset(5, 20.));
2401 state.remeasure_items(5..6);
2402 draw(cx);
2403 assert!(state.0.borrow().is_provisional(), "{name}");
2404 navigate(&state);
2405 assert!(!state.0.borrow().is_provisional(), "{name}");
2406 draw(cx);
2407 assert!(!state.0.borrow().is_provisional(), "{name}");
2408 assert_eq!(
2409 painted
2410 .borrow()
2411 .iter()
2412 .find(|(index, _, _)| *index == destination)
2413 .copied(),
2414 Some((destination, px(top), px(top + 50.))),
2415 "{name}, measured={measured}",
2416 );
2417 }
2418 }
2419
2420 let navigations: [(&str, fn(&ListState)); 6] = [
2421 ("end_sentinel", |state| state.scroll_to(list_offset(3, 0.))),
2422 ("scroll_to_end", ListState::scroll_to_end),
2423 ("follow_tail", |state| {
2424 state.set_follow_mode(FollowMode::Tail)
2425 }),
2426 ("horizontal", ListState::scroll_to_end),
2427 ("pause_tail", |state| {
2428 state.set_follow_mode(FollowMode::Tail)
2429 }),
2430 ("disable_tail", |state| {
2431 state.set_follow_mode(FollowMode::Tail)
2432 }),
2433 ];
2434 for (name, navigate) in navigations {
2435 let state = ListState::new(3, crate::ListAlignment::Top, px(0.)).measure_all();
2436 let ready = item_readiness(&state, false);
2437 let painted = Rc::new(RefCell::new(Vec::new()));
2438 let draw = |height, cx: &mut VisualTestContext| {
2439 painted.borrow_mut().clear();
2440 let painted = painted.clone();
2441 draw_list(
2442 &state,
2443 200.,
2444 move |index| painted_item(index, height, &painted),
2445 cx,
2446 );
2447 };
2448 navigate(&state);
2449 for (height, measurement_ready, expected) in [
2450 (
2451 100.,
2452 false,
2453 [(1, px(0.), px(100.)), (2, px(100.), px(200.))],
2454 ),
2455 (
2456 150.,
2457 true,
2458 [(1, px(-100.), px(50.)), (2, px(50.), px(200.))],
2459 ),
2460 ] {
2461 if measurement_ready {
2462 match name {
2463 "horizontal" => cx.simulate_event(ScrollEvent {
2464 position: point(px(50.), px(100.)),
2465 delta: ScrollDelta::Pixels(point(px(20.), px(0.))),
2466 ..ScrollEvent::default()
2467 }),
2468 "pause_tail" => state.pause_following_tail(),
2469 "disable_tail" => state.set_follow_mode(FollowMode::Normal),
2470 _ => {}
2471 }
2472 }
2473 ready.set(measurement_ready);
2474 draw(height, cx);
2475 if !measurement_ready {
2476 assert_eq!(
2477 state.scroll_px_offset_for_scrollbar().y,
2478 px(-100.),
2479 "{name}"
2480 );
2481 assert_eq!(state.logical_scroll_top(), list_offset(1, 0.), "{name}");
2482 assert_eq!(state.item_is_above_viewport(2), None, "{name}");
2483 }
2484 let paused = name == "pause_tail" || name == "disable_tail";
2485 let expected = if measurement_ready && paused {
2486 [(1, px(0.), px(150.)), (2, px(150.), px(300.))]
2487 } else {
2488 expected
2489 };
2490 assert_eq!(
2491 state.is_following_tail(),
2492 name == "follow_tail" || (paused && !measurement_ready),
2493 "{name}"
2494 );
2495 assert_eq!(painted.borrow().as_slice(), &expected, "{name}");
2496 }
2497 ready.set(false);
2498 navigate(&state);
2499 draw(150., cx);
2500 state.scroll_by(px(-20.));
2501 draw(150., cx);
2502 assert_eq!(state.logical_scroll_top(), list_offset(1, 80.), "{name}");
2503 assert!(!state.is_following_tail(), "{name}");
2504 assert_eq!(
2505 painted.borrow().as_slice(),
2506 &[(1, px(-80.), px(70.)), (2, px(70.), px(220.)),],
2507 "{name}"
2508 );
2509 }
2510 }
2511
2512 #[gpui::test]
2513 fn test_unready_overdraw_and_backfill_preserve_scroll_anchor(cx: &mut TestAppContext) {
2514 let cx = cx.add_empty_window();
2515 for (overdraw, anchor, unready_item) in
2516 [(100., list_offset(0, 0.), 2), (0., list_offset(2, 40.), 1)]
2517 {
2518 let state = ListState::new(3, crate::ListAlignment::Top, px(overdraw));
2519 let ready = Rc::new(Cell::new(false));
2520 state.set_item_measurement_readiness({
2521 let ready = ready.clone();
2522 move |index, _| index != unready_item || ready.get()
2523 });
2524 let painted = Rc::new(RefCell::new(Vec::new()));
2525 let draw = |height, cx: &mut VisualTestContext| {
2526 painted.borrow_mut().clear();
2527 let painted = painted.clone();
2528 draw_list(
2529 &state,
2530 200.,
2531 move |index| {
2532 let height = if index == unready_item { height } else { 100. };
2533 painted_item(index, height, &painted)
2534 },
2535 cx,
2536 );
2537 };
2538 state.set_follow_mode(FollowMode::Tail);
2539 state.scroll_to(anchor);
2540 draw(0., cx);
2541 assert_eq!(state.logical_scroll_top(), anchor);
2542 assert!(!state.is_following_tail());
2543 assert_eq!(
2544 *painted.borrow(),
2545 if unready_item == 2 {
2546 vec![(0, px(0.), px(100.)), (1, px(100.), px(200.))]
2547 } else {
2548 vec![(2, px(-40.), px(60.))]
2549 }
2550 );
2551 ready.set(true);
2552 draw(500., cx);
2553 if unready_item == 2 {
2554 assert_eq!(state.logical_scroll_top(), anchor);
2555 assert!(!state.is_following_tail());
2556 assert_eq!(state.max_offset_for_scrollbar().y, px(500.));
2557 assert_eq!(
2558 painted.borrow().as_slice(),
2559 &[(0, px(0.), px(100.)), (1, px(100.), px(200.))]
2560 );
2561 } else {
2562 assert_eq!(state.logical_scroll_top(), list_offset(1, 400.));
2563 assert!(state.is_following_tail());
2564 assert_eq!(
2565 painted.borrow().as_slice(),
2566 &[(1, px(-400.), px(100.)), (2, px(100.), px(200.))]
2567 );
2568 }
2569 }
2570 }
2571
2572 #[gpui::test]
2573 fn test_unready_overdraw_stops_at_first_pending_item(cx: &mut TestAppContext) {
2574 let cx = cx.add_empty_window();
2575 for anchor in [0, 10] {
2576 let state = ListState::new(13, crate::ListAlignment::Top, px(100.));
2577 let ready = Rc::new(Cell::new(false));
2578 let initially_ready = move |index| {
2579 if anchor == 0 { index < 2 } else { index >= 10 }
2580 };
2581 state.set_item_measurement_readiness({
2582 let ready = ready.clone();
2583 move |index, _| initially_ready(index) || ready.get()
2584 });
2585 state.scroll_to(list_offset(anchor, 0.));
2586 let rendered = Rc::new(RefCell::new(Vec::new()));
2587 let painted = Rc::new(RefCell::new(Vec::new()));
2588 let expected_frames = if anchor == 0 {
2589 [vec![0, 1, 2], vec![0, 1, 2], vec![0, 1, 2], vec![0, 1]]
2590 } else {
2591 [
2592 vec![10, 11, 12, 9],
2593 vec![10, 11, 9],
2594 vec![10, 11, 9],
2595 vec![10, 11],
2596 ]
2597 };
2598 for (frame, expected_rendered) in expected_frames.into_iter().enumerate() {
2599 let measurement_ready = frame >= 2;
2600 ready.set(measurement_ready);
2601 rendered.borrow_mut().clear();
2602 painted.borrow_mut().clear();
2603 draw_list(
2604 &state,
2605 200.,
2606 {
2607 let rendered = rendered.clone();
2608 let painted = painted.clone();
2609 move |index| {
2610 rendered.borrow_mut().push(index);
2611 let height = if initially_ready(index) || measurement_ready {
2612 100.
2613 } else {
2614 0.
2615 };
2616 painted_item(index, height, &painted)
2617 }
2618 },
2619 cx,
2620 );
2621 assert_eq!(*rendered.borrow(), expected_rendered);
2622 assert_eq!(state.item_count(), 13);
2623 assert_eq!(state.logical_scroll_top(), list_offset(anchor, 0.));
2624 assert_eq!(
2625 painted.borrow().as_slice(),
2626 &[(anchor, px(0.), px(100.)), (anchor + 1, px(100.), px(200.))]
2627 );
2628 }
2629 }
2630 }
2631
2632 #[gpui::test]
2633 fn test_scrollbar_and_reset_do_not_resurrect_provisional(cx: &mut TestAppContext) {
2634 let cx = cx.add_empty_window();
2635 let state = ListState::new(20, crate::ListAlignment::Top, px(0.));
2636 let painted = Rc::new(RefCell::new(Vec::new()));
2637 let draw = |cx: &mut VisualTestContext| {
2638 painted.borrow_mut().clear();
2639 let painted = painted.clone();
2640 draw_list(
2641 &state,
2642 200.,
2643 move |index| painted_item(index, if index == 5 { 0. } else { 50. }, &painted),
2644 cx,
2645 );
2646 };
2647 state.set_item_measurement_readiness(|index, _| index != 5);
2648 assert!(!state.0.borrow().is_provisional());
2649 state.set_follow_mode(FollowMode::Tail);
2650 for intervening_frame in [false, true] {
2651 state.scroll_to(list_offset(5, 20.));
2652 state.remeasure_items(5..6);
2653 draw(cx);
2654 state.scrollbar_drag_started();
2655 if intervening_frame {
2656 draw(cx);
2657 assert!(state.0.borrow().is_provisional());
2658 }
2659 state.set_offset_from_scrollbar(point(px(0.), px(-10_000.)));
2660 assert!(!state.0.borrow().is_provisional());
2661 state.scrollbar_drag_ended();
2662 assert!(!state.is_scrollbar_dragging());
2663 draw(cx);
2664 assert_eq!(
2665 painted.borrow().last().copied(),
2666 Some((19, px(150.), px(200.)))
2667 );
2668 assert!(!state.0.borrow().is_provisional());
2669 state.scroll_to(list_offset(5, 20.));
2670 assert!(!state.0.borrow().is_provisional());
2671 draw(cx);
2672 assert!(state.0.borrow().is_provisional());
2673 assert_eq!(state.logical_scroll_top(), list_offset(5, 20.));
2674 }
2675 state.remeasure_items(5..6);
2676 state.reset(20);
2677 assert!(!state.0.borrow().is_provisional());
2678 draw(cx);
2679 assert!(!state.0.borrow().is_provisional());
2680 assert_eq!(state.logical_scroll_top(), ListOffset::default());
2681 assert_eq!(
2682 painted.borrow().first().copied(),
2683 Some((0, px(0.), px(50.)))
2684 );
2685 }
2686
2687 #[gpui::test]
2688 fn test_splice_preserves_only_surviving_scroll_offsets(cx: &mut TestAppContext) {
2689 let cx = cx.add_empty_window();
2690 let painted = Rc::new(RefCell::new(Vec::new()));
2691 let draw = |state: &ListState, height, cx: &mut VisualTestContext| {
2692 painted.borrow_mut().clear();
2693 let painted = painted.clone();
2694 draw_list(
2695 state,
2696 200.,
2697 move |index| painted_item(index, height, &painted),
2698 cx,
2699 );
2700 };
2701 for (provisional, replacement_count) in [(true, 0), (false, 1)] {
2702 let state = ListState::new(20, crate::ListAlignment::Top, px(0.));
2703 let ready = item_readiness(&state, !provisional);
2704 state.scroll_to(list_offset(8, 50.));
2705 state.remeasure_items(8..9);
2706 if provisional {
2707 draw(&state, 100., cx);
2708 }
2709 state.splice(8..9, replacement_count);
2710 assert!(!state.0.borrow().is_provisional());
2711 ready.set(true);
2712 draw(&state, 100., cx);
2713 assert_eq!(state.logical_scroll_top(), list_offset(8, 0.));
2714 assert_eq!(
2715 painted.borrow().first().copied(),
2716 Some((8, px(0.), px(100.)))
2717 );
2718 }
2719 let state = ListState::new(20, crate::ListAlignment::Top, px(0.)).measure_all();
2720 state.scroll_to(list_offset(8, 50.));
2721 draw(&state, 100., cx);
2722 state.remeasure();
2723 state.splice(0..2, 0);
2724 draw(&state, 80., cx);
2725 assert_eq!(state.logical_scroll_top(), list_offset(6, 40.));
2726 assert_eq!(
2727 painted.borrow().first().copied(),
2728 Some((6, px(-40.), px(40.)))
2729 );
2730 let ready = item_readiness(&state, false);
2731 state.remeasure_items(6..7);
2732 state.splice(8..10, 3);
2733 draw(&state, 100., cx);
2734 assert!(state.0.borrow().is_provisional());
2735 assert_eq!(state.logical_scroll_top(), list_offset(6, 40.));
2736 state.splice(0..0, 2);
2737 assert!(!state.0.borrow().is_provisional());
2738 draw(&state, 100., cx);
2739 assert!(state.0.borrow().is_provisional());
2740 assert_eq!(state.logical_scroll_top(), list_offset(8, 40.));
2741 ready.set(true);
2742 draw(&state, 100., cx);
2743 assert_eq!(state.logical_scroll_top(), list_offset(8, 40.));
2744 assert_eq!(
2745 painted.borrow().first().copied(),
2746 Some((8, px(-40.), px(60.)))
2747 );
2748 }
2749
2750 #[gpui::test]
2751 fn test_child_autoscroll_overrides_pending_and_release_fills_viewport(cx: &mut TestAppContext) {
2752 let cx = cx.add_empty_window();
2753 for (count, child, request, expected, released) in [
2754 (10, 2, 40.0..45.0, list_offset(2, 40.), list_offset(2, 40.)),
2755 (3, 2, 40.0..45.0, list_offset(2, 40.), list_offset(1, 0.)),
2756 (10, 4, 80.0..90.0, list_offset(2, 90.), list_offset(2, 90.)),
2757 (10, 2, -130.0..5.0, list_offset(0, 70.), list_offset(0, 70.)),
2758 (10, 2, -250.0..5.0, list_offset(0, 0.), list_offset(0, 0.)),
2759 ] {
2760 let state = ListState::new(count, crate::ListAlignment::Top, px(0.));
2761 let requested = Rc::new(Cell::new(false));
2762 let painted = Rc::new(RefCell::new(Vec::new()));
2763 let draw = |cx: &mut VisualTestContext| {
2764 painted.borrow_mut().clear();
2765 let painted = painted.clone();
2766 let requested = requested.clone();
2767 let request = request.clone();
2768 draw_list(
2769 &state,
2770 200.,
2771 move |index| {
2772 let painted = painted.clone();
2773 let requested = requested.clone();
2774 let request = request.clone();
2775 canvas(
2776 move |bounds, window, _| {
2777 if index == child && !requested.replace(true) {
2778 window.request_autoscroll(Bounds::from_corners(
2779 point(bounds.left(), bounds.top() + px(request.start)),
2780 point(bounds.right(), bounds.top() + px(request.end)),
2781 ));
2782 }
2783 },
2784 move |bounds, _, _, _| {
2785 painted
2786 .borrow_mut()
2787 .push((index, bounds.top(), bounds.bottom()));
2788 },
2789 )
2790 .h(px(100.))
2791 .w_full()
2792 .into_any()
2793 },
2794 cx,
2795 );
2796 };
2797 let ready = item_readiness(&state, false);
2798 state.scroll_to(list_offset(2, 50.));
2799 state.remeasure_items(2..3);
2800 for _ in 0..2 {
2801 draw(cx);
2802 assert!(requested.get());
2803 assert!(state.0.borrow().is_provisional());
2804 assert_eq!(state.logical_scroll_top(), expected);
2805 assert_eq!(
2806 painted.borrow().first().copied(),
2807 Some((
2808 expected.item_ix,
2809 -expected.offset_in_item,
2810 px(100.) - expected.offset_in_item,
2811 ))
2812 );
2813 }
2814 ready.set(true);
2815 draw(cx);
2816 assert!(!state.0.borrow().is_provisional());
2817 assert_eq!(state.logical_scroll_top(), released);
2818 let visible_count = if released.offset_in_item == px(0.) {
2819 2
2820 } else {
2821 3
2822 };
2823 assert_eq!(
2824 painted.borrow().as_slice(),
2825 (0..visible_count)
2826 .map(|index| {
2827 let top = px(index as f32 * 100.) - released.offset_in_item;
2828 (released.item_ix + index, top, top + px(100.))
2829 })
2830 .collect::<Vec<_>>()
2831 .as_slice()
2832 );
2833 }
2834 }
2835
2836 #[gpui::test]
2837 fn test_remeasure_then_scroll_does_not_revert_scroll_position(cx: &mut TestAppContext) {
2838 let cx = cx.add_empty_window();
2839
2840 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2841
2842 struct TestView(ListState);
2843 impl Render for TestView {
2844 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2845 list(self.0.clone(), |_, _, _| {
2846 div().h(px(100.)).w_full().into_any()
2847 })
2848 .w_full()
2849 .h_full()
2850 }
2851 }
2852
2853 let view = {
2854 let state = state.clone();
2855 cx.update(|_, cx| cx.new(|_| TestView(state)))
2856 };
2857
2858 state.scroll_to(gpui::ListOffset {
2859 item_ix: 5,
2860 offset_in_item: px(40.),
2861 });
2862
2863 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2864 view.clone().into_any_element()
2865 });
2866
2867 state.remeasure_items(5..6);
2868
2869 cx.simulate_event(ScrollEvent {
2870 position: point(px(50.), px(100.)),
2871 delta: ScrollDelta::Pixels(point(px(0.), px(-30.))),
2872 ..Default::default()
2873 });
2874
2875 let offset = state.logical_scroll_top();
2876 assert_eq!(offset.item_ix, 5);
2877 assert_eq!(offset.offset_in_item, px(70.));
2878
2879 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2880 view.into_any_element()
2881 });
2882
2883 let offset = state.logical_scroll_top();
2884 assert_eq!(offset.item_ix, 5);
2885 assert_eq!(
2886 offset.offset_in_item,
2887 px(70.),
2888 "scrolling after a remeasure should not be reverted by the stale pending scroll"
2889 );
2890 }
2891
2892 #[gpui::test]
2893 fn test_scroll_after_remeasure_clamps_to_shrunk_item_height(cx: &mut TestAppContext) {
2894 let cx = cx.add_empty_window();
2895 for provisional in [false, true] {
2896 let state = ListState::new(20, crate::ListAlignment::Top, px(10.));
2897 let painted = Rc::new(RefCell::new(Vec::new()));
2898 let draw = |height, cx: &mut VisualTestContext| {
2899 painted.borrow_mut().clear();
2900 let painted = painted.clone();
2901 draw_list(
2902 &state,
2903 200.,
2904 move |index| {
2905 painted_item(index, if index == 5 { height } else { 100. }, &painted)
2906 },
2907 cx,
2908 );
2909 };
2910 let ready = item_readiness(&state, true);
2911 state.scroll_to(list_offset(5, 40.));
2912 draw(100., cx);
2913 ready.set(!provisional);
2914 state.remeasure_items(5..6);
2915 if provisional {
2916 draw(100., cx);
2917 }
2918 ready.set(true);
2919 cx.simulate_event(ScrollEvent {
2920 position: point(px(50.), px(100.)),
2921 delta: ScrollDelta::Pixels(point(px(0.), px(-30.))),
2922 ..ScrollEvent::default()
2923 });
2924 assert_eq!(state.logical_scroll_top(), list_offset(5, 70.));
2925 for _ in 0..2 {
2926 draw(50., cx);
2927 assert_eq!(state.logical_scroll_top(), list_offset(5, 50.));
2928 assert_eq!(
2929 painted
2930 .borrow()
2931 .iter()
2932 .find(|(index, _, _)| *index == 6)
2933 .copied(),
2934 Some((6, px(0.), px(100.)))
2935 );
2936 }
2937 }
2938 }
2939
2940 #[gpui::test]
2941 fn test_follow_tail_stays_at_bottom_as_items_grow(cx: &mut TestAppContext) {
2942 let cx = cx.add_empty_window();
2943
2944 let item_height = Rc::new(Cell::new(50usize));
2947 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
2948
2949 struct TestView {
2950 state: ListState,
2951 item_height: Rc<Cell<usize>>,
2952 }
2953 impl Render for TestView {
2954 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2955 let height = self.item_height.get();
2956 list(self.state.clone(), move |_, _, _| {
2957 div().h(px(height as f32)).w_full().into_any()
2958 })
2959 .w_full()
2960 .h_full()
2961 }
2962 }
2963
2964 let state_clone = state.clone();
2965 let item_height_clone = item_height.clone();
2966 let view = cx.update(|_, cx| {
2967 cx.new(|_| TestView {
2968 state: state_clone,
2969 item_height: item_height_clone,
2970 })
2971 });
2972
2973 state.set_follow_mode(FollowMode::Tail);
2974
2975 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2978 view.clone().into_any_element()
2979 });
2980
2981 let offset = state.logical_scroll_top();
2984 assert_eq!(offset.item_ix, 6);
2985 assert_eq!(offset.offset_in_item, px(0.));
2986 assert!(state.is_following_tail());
2987
2988 item_height.set(80);
2991 state.remeasure();
2992
2993 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
2994 view.into_any_element()
2995 });
2996
2997 let offset = state.logical_scroll_top();
3004 assert_eq!(offset.item_ix, 7);
3005 assert_eq!(offset.offset_in_item, px(40.));
3006 assert!(state.is_following_tail());
3007 }
3008
3009 #[gpui::test]
3010 fn test_pause_following_tail_reengages_when_still_at_bottom(cx: &mut TestAppContext) {
3011 let cx = cx.add_empty_window();
3012
3013 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
3015
3016 struct TestView(ListState);
3017 impl Render for TestView {
3018 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3019 list(self.0.clone(), |_, _, _| {
3020 div().h(px(50.)).w_full().into_any()
3021 })
3022 .w_full()
3023 .h_full()
3024 }
3025 }
3026
3027 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3028 state.set_follow_mode(FollowMode::Tail);
3029
3030 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3031 view.clone().into_any_element()
3032 });
3033 assert!(state.is_following_tail());
3034
3035 state.pause_following_tail();
3039 assert!(!state.is_following_tail());
3040
3041 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3042 view.into_any_element()
3043 });
3044 assert!(
3045 state.is_following_tail(),
3046 "pausing while at the bottom must re-engage follow-tail on the next layout"
3047 );
3048 }
3049
3050 #[gpui::test]
3051 fn test_pause_following_tail_freezes_off_bottom(cx: &mut TestAppContext) {
3052 let cx = cx.add_empty_window();
3053
3054 let item_height = Rc::new(Cell::new(50usize));
3057 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
3058
3059 struct TestView {
3060 state: ListState,
3061 item_height: Rc<Cell<usize>>,
3062 }
3063 impl Render for TestView {
3064 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3065 let height = self.item_height.get();
3066 list(self.state.clone(), move |_, _, _| {
3067 div().h(px(height as f32)).w_full().into_any()
3068 })
3069 .w_full()
3070 .h_full()
3071 }
3072 }
3073
3074 let view = cx.update(|_, cx| {
3075 cx.new(|_| TestView {
3076 state: state.clone(),
3077 item_height: item_height.clone(),
3078 })
3079 });
3080 state.set_follow_mode(FollowMode::Tail);
3081
3082 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3084 view.clone().into_any_element()
3085 });
3086 assert_eq!(state.logical_scroll_top().item_ix, 6);
3087 assert!(state.is_following_tail());
3088
3089 state.pause_following_tail();
3093 item_height.set(80);
3094 state.remeasure();
3095 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3096 view.clone().into_any_element()
3097 });
3098 let offset = state.logical_scroll_top();
3099 assert_eq!(offset.item_ix, 6);
3100 assert_eq!(offset.offset_in_item, px(0.));
3101 assert!(
3102 !state.is_following_tail(),
3103 "a paused list must not re-engage while the frozen top is off the bottom"
3104 );
3105
3106 item_height.set(50);
3109 state.remeasure();
3110 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3111 view.into_any_element()
3112 });
3113 assert!(
3114 state.is_following_tail(),
3115 "returning to the bottom must restore follow-tail"
3116 );
3117 }
3118
3119 #[gpui::test]
3120 fn test_follow_tail_disengages_on_user_scroll(cx: &mut TestAppContext) {
3121 let cx = cx.add_empty_window();
3122
3123 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
3125
3126 struct TestView(ListState);
3127 impl Render for TestView {
3128 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3129 list(self.0.clone(), |_, _, _| {
3130 div().h(px(50.)).w_full().into_any()
3131 })
3132 .w_full()
3133 .h_full()
3134 }
3135 }
3136
3137 state.set_follow_mode(FollowMode::Tail);
3138
3139 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, cx| {
3141 cx.new(|_| TestView(state.clone())).into_any_element()
3142 });
3143 assert!(state.is_following_tail());
3144
3145 cx.simulate_event(ScrollEvent {
3148 position: point(px(50.), px(100.)),
3149 delta: ScrollDelta::Pixels(point(px(0.), px(100.))),
3150 ..Default::default()
3151 });
3152
3153 assert!(
3154 !state.is_following_tail(),
3155 "follow-tail should disengage when the user scrolls toward the start"
3156 );
3157 }
3158
3159 #[gpui::test]
3160 fn test_follow_tail_disengages_on_scrollbar_reposition(cx: &mut TestAppContext) {
3161 let cx = cx.add_empty_window();
3162
3163 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3165
3166 struct TestView(ListState);
3167 impl Render for TestView {
3168 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3169 list(self.0.clone(), |_, _, _| {
3170 div().h(px(50.)).w_full().into_any()
3171 })
3172 .w_full()
3173 .h_full()
3174 }
3175 }
3176
3177 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3178
3179 state.set_follow_mode(FollowMode::Tail);
3180
3181 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3183 view.clone().into_any_element()
3184 });
3185 assert!(state.is_following_tail());
3186
3187 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
3189
3190 let offset = state.logical_scroll_top();
3191 assert_eq!(offset.item_ix, 3);
3192 assert_eq!(offset.offset_in_item, px(0.));
3193 assert!(
3194 !state.is_following_tail(),
3195 "follow-tail should disengage when the scrollbar manually repositions the list"
3196 );
3197
3198 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3201 view.into_any_element()
3202 });
3203
3204 let offset = state.logical_scroll_top();
3205 assert_eq!(offset.item_ix, 3);
3206 assert_eq!(offset.offset_in_item, px(0.));
3207 }
3208
3209 #[gpui::test]
3210 fn test_scrollbar_drag_with_growing_content(cx: &mut TestAppContext) {
3211 let cx = cx.add_empty_window();
3212
3213 let last_item_height = Rc::new(Cell::new(50usize));
3214 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3215
3216 struct TestView {
3217 state: ListState,
3218 last_item_height: Rc<Cell<usize>>,
3219 }
3220 impl Render for TestView {
3221 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3222 let last_item_height = self.last_item_height.clone();
3223 list(self.state.clone(), move |index, _, _| {
3224 let height = if index == 9 {
3225 last_item_height.get()
3226 } else {
3227 50
3228 };
3229 div().h(px(height as f32)).w_full().into_any()
3230 })
3231 .w_full()
3232 .h_full()
3233 }
3234 }
3235
3236 let view = cx.update(|_, cx| {
3237 cx.new(|_| TestView {
3238 state: state.clone(),
3239 last_item_height: last_item_height.clone(),
3240 })
3241 });
3242
3243 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3244 view.clone().into_any_element()
3245 });
3246
3247 state.scrollbar_drag_started();
3248
3249 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
3250 let scrollbar_offset_before_growth = state.scroll_px_offset_for_scrollbar();
3251
3252 let offset = state.logical_scroll_top();
3253 assert_eq!(offset.item_ix, 3);
3254 assert_eq!(offset.offset_in_item, px(0.));
3255
3256 last_item_height.set(550);
3257 state.remeasure_items(9..10);
3258 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3259 view.clone().into_any_element()
3260 });
3261
3262 assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
3263 assert_eq!(
3264 state.scroll_px_offset_for_scrollbar(),
3265 scrollbar_offset_before_growth
3266 );
3267
3268 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
3269 let offset = state.logical_scroll_top();
3270 assert_eq!(offset.item_ix, 3);
3271 assert_eq!(offset.offset_in_item, px(0.));
3272 }
3273
3274 #[gpui::test]
3275 fn test_set_follow_tail_snaps_to_bottom(cx: &mut TestAppContext) {
3276 let cx = cx.add_empty_window();
3277
3278 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
3280
3281 struct TestView(ListState);
3282 impl Render for TestView {
3283 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3284 list(self.0.clone(), |_, _, _| {
3285 div().h(px(50.)).w_full().into_any()
3286 })
3287 .w_full()
3288 .h_full()
3289 }
3290 }
3291
3292 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3293
3294 state.scroll_to(gpui::ListOffset {
3296 item_ix: 3,
3297 offset_in_item: px(0.),
3298 });
3299
3300 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3301 view.clone().into_any_element()
3302 });
3303
3304 let offset = state.logical_scroll_top();
3305 assert_eq!(offset.item_ix, 3);
3306 assert_eq!(offset.offset_in_item, px(0.));
3307 assert!(!state.is_following_tail());
3308
3309 state.set_follow_mode(FollowMode::Tail);
3312
3313 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3314 view.into_any_element()
3315 });
3316
3317 let offset = state.logical_scroll_top();
3320 assert_eq!(offset.item_ix, 6);
3321 assert_eq!(offset.offset_in_item, px(0.));
3322 assert!(state.is_following_tail());
3323 }
3324
3325 #[gpui::test]
3326 fn test_bottom_aligned_scrollbar_offset_at_end(cx: &mut TestAppContext) {
3327 let cx = cx.add_empty_window();
3328
3329 const ITEMS: usize = 10;
3330 const ITEM_SIZE: f32 = 50.0;
3331
3332 let state = ListState::new(
3333 ITEMS,
3334 crate::ListAlignment::Bottom,
3335 px(ITEMS as f32 * ITEM_SIZE),
3336 );
3337
3338 struct TestView(ListState);
3339 impl Render for TestView {
3340 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3341 list(self.0.clone(), |_, _, _| {
3342 div().h(px(ITEM_SIZE)).w_full().into_any()
3343 })
3344 .w_full()
3345 .h_full()
3346 }
3347 }
3348
3349 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
3350 cx.new(|_| TestView(state.clone())).into_any_element()
3351 });
3352
3353 assert_eq!(state.logical_scroll_top().item_ix, ITEMS);
3356
3357 let max_offset = state.max_offset_for_scrollbar();
3358 let scroll_offset = state.scroll_px_offset_for_scrollbar();
3359
3360 assert_eq!(
3361 -scroll_offset.y, max_offset.y,
3362 "scrollbar offset ({}) should equal max offset ({}) when list is pinned to bottom",
3363 -scroll_offset.y, max_offset.y,
3364 );
3365 }
3366
3367 #[gpui::test]
3371 fn test_follow_tail_reengages_when_scrolled_back_to_bottom(cx: &mut TestAppContext) {
3372 let cx = cx.add_empty_window();
3373
3374 let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
3376
3377 struct TestView(ListState);
3378 impl Render for TestView {
3379 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3380 list(self.0.clone(), |_, _, _| {
3381 div().h(px(50.)).w_full().into_any()
3382 })
3383 .w_full()
3384 .h_full()
3385 }
3386 }
3387
3388 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3389
3390 state.set_follow_mode(FollowMode::Tail);
3391
3392 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3393 view.clone().into_any_element()
3394 });
3395 assert!(state.is_following_tail());
3396
3397 cx.simulate_event(ScrollEvent {
3399 position: point(px(50.), px(100.)),
3400 delta: ScrollDelta::Pixels(point(px(0.), px(50.))),
3401 ..Default::default()
3402 });
3403 assert!(!state.is_following_tail());
3404
3405 cx.simulate_event(ScrollEvent {
3407 position: point(px(50.), px(100.)),
3408 delta: ScrollDelta::Pixels(point(px(0.), px(-10000.))),
3409 ..Default::default()
3410 });
3411
3412 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3415 view.clone().into_any_element()
3416 });
3417 assert!(
3418 state.is_following_tail(),
3419 "follow_tail should re-engage after scrolling back to the bottom"
3420 );
3421 }
3422
3423 #[gpui::test]
3426 fn test_follow_tail_reengagement_not_fooled_by_unmeasured_items(cx: &mut TestAppContext) {
3427 let cx = cx.add_empty_window();
3428
3429 let state = ListState::new(20, crate::ListAlignment::Top, px(1000.));
3433
3434 struct TestView(ListState);
3435 impl Render for TestView {
3436 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3437 list(self.0.clone(), |_, _, _| {
3438 div().h(px(50.)).w_full().into_any()
3439 })
3440 .w_full()
3441 .h_full()
3442 }
3443 }
3444
3445 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3446
3447 state.set_follow_mode(FollowMode::Tail);
3448
3449 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3450 view.clone().into_any_element()
3451 });
3452 assert!(state.is_following_tail());
3453
3454 cx.simulate_event(ScrollEvent {
3458 position: point(px(50.), px(100.)),
3459 delta: ScrollDelta::Pixels(point(px(0.), px(200.))),
3460 ..Default::default()
3461 });
3462 assert!(!state.is_following_tail());
3463
3464 state.remeasure_items(19..20);
3468
3469 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3475 view.clone().into_any_element()
3476 });
3477 assert!(
3478 !state.is_following_tail(),
3479 "follow_tail should not falsely re-engage due to an unmeasured item \
3480 reducing items.summary().height"
3481 );
3482 }
3483
3484 #[gpui::test]
3485 fn test_follow_tail_reengages_after_scrollbar_disengagement(cx: &mut TestAppContext) {
3486 let cx = cx.add_empty_window();
3487
3488 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3490
3491 struct TestView(ListState);
3492 impl Render for TestView {
3493 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3494 list(self.0.clone(), |_, _, _| {
3495 div().h(px(50.)).w_full().into_any()
3496 })
3497 .w_full()
3498 .h_full()
3499 }
3500 }
3501
3502 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3503
3504 state.set_follow_mode(FollowMode::Tail);
3505 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3506 view.clone().into_any_element()
3507 });
3508 assert!(state.is_following_tail());
3509
3510 state.set_offset_from_scrollbar(point(px(0.), px(-150.)));
3512 assert!(!state.is_following_tail());
3513
3514 state.set_offset_from_scrollbar(point(px(0.), px(-300.)));
3517 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3518 view.into_any_element()
3519 });
3520 assert!(
3521 state.is_following_tail(),
3522 "follow_tail should re-engage after scrolling back to the bottom via the scrollbar"
3523 );
3524 }
3525
3526 #[gpui::test]
3527 fn test_follow_tail_reengages_after_scrollbar_drag_to_bottom_while_growing(
3528 cx: &mut TestAppContext,
3529 ) {
3530 let cx = cx.add_empty_window();
3531
3532 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3533
3534 struct TestView(ListState);
3535 impl Render for TestView {
3536 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3537 list(self.0.clone(), |_, _, _| {
3538 div().h(px(50.)).w_full().into_any()
3539 })
3540 .w_full()
3541 .h_full()
3542 }
3543 }
3544
3545 let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
3546
3547 state.set_follow_mode(FollowMode::Tail);
3548 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3549 view.clone().into_any_element()
3550 });
3551 assert!(state.is_following_tail());
3552
3553 state.scrollbar_drag_started();
3554
3555 state.splice(10..10, 10);
3556 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3557 view.clone().into_any_element()
3558 });
3559
3560 state.set_offset_from_scrollbar(point(px(0.), px(-300.)));
3561 state.scrollbar_drag_ended();
3562
3563 cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
3564 view.into_any_element()
3565 });
3566
3567 assert!(
3568 state.is_following_tail(),
3569 "follow_tail should re-engage when the user drags the scrollbar to \
3570 the bottom of its track, even when content has grown during the drag \
3571 (so frozen_bottom < live_bottom)"
3572 );
3573 }
3574
3575 #[gpui::test]
3576 fn test_horizontal_padding_insets_items(cx: &mut TestAppContext) {
3577 let cx = cx.add_empty_window();
3578
3579 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3580 let item_bounds = Rc::new(RefCell::new(Vec::new()));
3581
3582 struct TestView {
3583 state: ListState,
3584 item_bounds: Rc<RefCell<Vec<Bounds<Pixels>>>>,
3585 }
3586 impl Render for TestView {
3587 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3588 let item_bounds = self.item_bounds.clone();
3589 list(self.state.clone(), move |_, _, _| {
3590 let item_bounds = item_bounds.clone();
3591 canvas(
3592 move |bounds, _, _| item_bounds.borrow_mut().push(bounds),
3593 |_, _, _, _| {},
3594 )
3595 .w_full()
3596 .aspect_ratio(2.)
3597 .into_any()
3598 })
3599 .pl(px(10.))
3600 .pr(px(30.))
3601 .w_full()
3602 .h_full()
3603 }
3604 }
3605
3606 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
3607 cx.new(|_| TestView {
3608 state: state.clone(),
3609 item_bounds: item_bounds.clone(),
3610 })
3611 .into_any_element()
3612 });
3613
3614 for bounds in item_bounds.borrow().iter() {
3615 assert_eq!(bounds.origin.x, px(10.));
3616 assert_eq!(bounds.size.width, px(60.));
3617 }
3618 assert_eq!(state.max_offset_for_scrollbar().y, px(200.));
3620 }
3621
3622 #[gpui::test]
3623 fn test_inferred_width_includes_horizontal_padding(cx: &mut TestAppContext) {
3624 let cx = cx.add_empty_window();
3625
3626 let state = ListState::new(2, crate::ListAlignment::Top, px(10.));
3627 let full_width_item_bounds = Rc::new(RefCell::new(None));
3628
3629 struct TestView {
3630 state: ListState,
3631 full_width_item_bounds: Rc<RefCell<Option<Bounds<Pixels>>>>,
3632 }
3633 impl Render for TestView {
3634 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3635 let full_width_item_bounds = self.full_width_item_bounds.clone();
3636 div().flex().size_full().child(
3637 list(self.state.clone(), move |ix, _, _| {
3638 if ix == 0 {
3639 div().w(px(50.)).h(px(20.)).into_any()
3640 } else {
3641 let full_width_item_bounds = full_width_item_bounds.clone();
3642 canvas(
3643 move |bounds, _, _| {
3644 *full_width_item_bounds.borrow_mut() = Some(bounds)
3645 },
3646 |_, _, _, _| {},
3647 )
3648 .h(px(20.))
3649 .w_full()
3650 .into_any()
3651 }
3652 })
3653 .with_sizing_behavior(ListSizingBehavior::Infer)
3654 .pl(px(10.))
3655 .pr(px(30.))
3656 .h_full(),
3657 )
3658 }
3659 }
3660
3661 cx.draw(point(px(0.), px(0.)), size(px(200.), px(100.)), |_, cx| {
3662 cx.new(|_| TestView {
3663 state: state.clone(),
3664 full_width_item_bounds: full_width_item_bounds.clone(),
3665 })
3666 .into_any_element()
3667 });
3668
3669 let bounds = full_width_item_bounds
3670 .borrow()
3671 .expect("item 1 was not painted");
3672 assert_eq!(bounds.origin.x, px(10.));
3673 assert_eq!(bounds.size.width, px(50.));
3674 }
3675
3676 #[gpui::test]
3677 fn test_autoscroll_above_item_top_with_horizontal_padding(cx: &mut TestAppContext) {
3678 let cx = cx.add_empty_window();
3679
3680 let state = ListState::new(5, crate::ListAlignment::Top, px(0.));
3681 state.scroll_to(gpui::ListOffset {
3682 item_ix: 2,
3683 offset_in_item: px(0.),
3684 });
3685
3686 struct TestView(ListState);
3687 impl Render for TestView {
3688 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3689 list(self.0.clone(), |ix, _, _| {
3690 if ix == 2 {
3691 canvas(
3692 |bounds, window, _| {
3693 window.request_autoscroll(Bounds::from_corners(
3694 point(bounds.left(), bounds.top() - px(30.)),
3695 point(bounds.right(), bounds.top() + px(5.)),
3696 ));
3697 },
3698 |_, _, _, _| {},
3699 )
3700 .w_full()
3701 .aspect_ratio(2.)
3702 .into_any()
3703 } else {
3704 div().w_full().aspect_ratio(2.).into_any()
3705 }
3706 })
3707 .pl(px(10.))
3708 .pr(px(30.))
3709 .w_full()
3710 .h_full()
3711 }
3712 }
3713
3714 cx.draw(point(px(0.), px(0.)), size(px(100.), px(60.)), |_, cx| {
3715 cx.new(|_| TestView(state.clone())).into_any_element()
3716 });
3717
3718 let scroll_top = state.logical_scroll_top();
3720 assert_eq!(scroll_top.item_ix, 1);
3721 assert_eq!(scroll_top.offset_in_item, px(0.));
3722 }
3723
3724 #[gpui::test]
3725 fn test_horizontal_padding_change_remeasures_items(cx: &mut TestAppContext) {
3726 let cx = cx.add_empty_window();
3727
3728 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3729
3730 struct TestView {
3731 state: ListState,
3732 padding_right: Pixels,
3733 }
3734 impl Render for TestView {
3735 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3736 list(self.state.clone(), |_, _, _| {
3737 div().w_full().aspect_ratio(2.).into_any()
3738 })
3739 .pl(px(10.))
3740 .pr(self.padding_right)
3741 .w_full()
3742 .h_full()
3743 }
3744 }
3745
3746 for (padding_right, expected_max_offset) in [(px(30.), px(200.)), (px(10.), px(300.))] {
3749 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
3750 cx.new(|_| TestView {
3751 state: state.clone(),
3752 padding_right,
3753 })
3754 .into_any_element()
3755 });
3756 assert_eq!(state.max_offset_for_scrollbar().y, expected_max_offset);
3757 }
3758 }
3759
3760 #[gpui::test]
3761 fn test_border_and_padding_inset_items(cx: &mut TestAppContext) {
3762 let cx = cx.add_empty_window();
3763
3764 let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
3765 let item_bounds = Rc::new(RefCell::new(Vec::new()));
3766
3767 struct TestView {
3768 state: ListState,
3769 item_bounds: Rc<RefCell<Vec<Bounds<Pixels>>>>,
3770 }
3771 impl Render for TestView {
3772 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3773 let item_bounds = self.item_bounds.clone();
3774 list(self.state.clone(), move |_, _, _| {
3775 let item_bounds = item_bounds.clone();
3776 canvas(
3777 move |bounds, _, _| item_bounds.borrow_mut().push(bounds),
3778 |_, _, _, _| {},
3779 )
3780 .w_full()
3781 .aspect_ratio(2.)
3782 .into_any()
3783 })
3784 .border_l_4()
3785 .pl(px(6.))
3786 .border_r_8()
3787 .pr(px(22.))
3788 .w_full()
3789 .h_full()
3790 }
3791 }
3792
3793 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
3794 cx.new(|_| TestView {
3795 state: state.clone(),
3796 item_bounds: item_bounds.clone(),
3797 })
3798 .into_any_element()
3799 });
3800
3801 let painted_bounds = item_bounds.borrow();
3802 assert!(!painted_bounds.is_empty());
3803 for bounds in painted_bounds.iter() {
3804 assert_eq!(bounds.origin.x, px(10.));
3805 assert_eq!(bounds.size.width, px(60.));
3806 }
3807 assert_eq!(state.max_offset_for_scrollbar().y, px(200.));
3808
3809 let reported_bounds = state.bounds_for_item(0).expect("item 0 should be measured");
3810 assert_eq!(reported_bounds.origin.x, px(10.));
3811 assert_eq!(reported_bounds.size.width, px(60.));
3812 }
3813
3814 fn draw_list(
3815 state: &ListState,
3816 viewport_height: f32,
3817 render_item: impl Fn(usize) -> AnyElement + 'static,
3818 cx: &mut VisualTestContext,
3819 ) {
3820 cx.draw(
3821 point(px(0.), px(0.)),
3822 size(px(100.), px(viewport_height)),
3823 |_, cx| {
3824 cx.new(|_| TestView {
3825 state: state.clone(),
3826 render_item: Rc::new(render_item),
3827 })
3828 .into_any_element()
3829 },
3830 );
3831 }
3832
3833 fn painted_item(
3834 index: usize,
3835 height: f32,
3836 painted: &Rc<RefCell<Vec<(usize, Pixels, Pixels)>>>,
3837 ) -> AnyElement {
3838 let painted = painted.clone();
3839 canvas(
3840 |_, _, _| {},
3841 move |bounds, _, _, _| {
3842 painted
3843 .borrow_mut()
3844 .push((index, bounds.top(), bounds.bottom()));
3845 },
3846 )
3847 .h(px(height))
3848 .w_full()
3849 .into_any()
3850 }
3851
3852 fn item_readiness(state: &ListState, ready: bool) -> Rc<Cell<bool>> {
3853 let ready = Rc::new(Cell::new(ready));
3854 state.set_item_measurement_readiness({
3855 let ready = ready.clone();
3856 move |_, _| ready.get()
3857 });
3858 ready
3859 }
3860
3861 fn list_offset(item_ix: usize, offset: f32) -> ListOffset {
3862 ListOffset {
3863 item_ix,
3864 offset_in_item: px(offset),
3865 }
3866 }
3867
3868 struct TestView {
3869 state: ListState,
3870 render_item: Rc<dyn Fn(usize) -> AnyElement>,
3871 }
3872
3873 impl Render for TestView {
3874 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3875 let render_item = self.render_item.clone();
3876 list(self.state.clone(), move |index, _, _| render_item(index))
3877 .w_full()
3878 .h_full()
3879 }
3880 }
3881
3882 struct TestListView(ListState);
3883 impl Render for TestListView {
3884 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3885 list(self.0.clone(), |_, _, _| {
3886 div().h(px(20.)).w_full().into_any()
3887 })
3888 .w_full()
3889 .h_full()
3890 }
3891 }
3892}