Skip to main content

gpui_core/elements/
list.rs

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