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}
  75
  76/// Whether the list is scrolling from top to bottom or bottom to top.
  77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
  78pub enum ListAlignment {
  79    /// The list is scrolling from top to bottom, like most lists.
  80    Top,
  81    /// The list is scrolling from bottom to top, like a chat log.
  82    Bottom,
  83}
  84
  85/// A scroll event that has been converted to be in terms of the list's items.
  86pub struct ListScrollEvent {
  87    /// The range of items currently visible in the list, after applying the scroll event.
  88    pub visible_range: Range<usize>,
  89
  90    /// The number of items that are currently visible in the list, after applying the scroll event.
  91    pub count: usize,
  92
  93    /// Whether the list has been scrolled.
  94    pub is_scrolled: bool,
  95}
  96
  97/// The sizing behavior to apply during layout.
  98#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
  99pub enum ListSizingBehavior {
 100    /// The list should calculate its size based on the size of its items.
 101    Infer,
 102    /// The list should not calculate a fixed size.
 103    #[default]
 104    Auto,
 105}
 106
 107/// The measuring behavior to apply during layout.
 108#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
 109pub enum ListMeasuringBehavior {
 110    /// Measure all items in the list.
 111    /// Note: This can be expensive for the first frame in a large list.
 112    Measure(bool),
 113    /// Only measure visible items
 114    #[default]
 115    Visible,
 116}
 117
 118impl ListMeasuringBehavior {
 119    fn reset(&mut self) {
 120        match self {
 121            ListMeasuringBehavior::Measure(has_measured) => *has_measured = false,
 122            ListMeasuringBehavior::Visible => {}
 123        }
 124    }
 125}
 126
 127/// The horizontal sizing behavior to apply during layout.
 128#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
 129pub enum ListHorizontalSizingBehavior {
 130    /// List items' width can never exceed the width of the list.
 131    #[default]
 132    FitList,
 133    /// List items' width may go over the width of the list, if any item is wider.
 134    Unconstrained,
 135}
 136
 137struct LayoutItemsResponse {
 138    max_item_width: Pixels,
 139    scroll_top: ListOffset,
 140    item_layouts: VecDeque<ItemLayout>,
 141}
 142
 143struct ItemLayout {
 144    index: usize,
 145    element: AnyElement,
 146    size: Size<Pixels>,
 147}
 148
 149/// Frame state used by the [List] element after layout.
 150pub struct ListPrepaintState {
 151    hitbox: Hitbox,
 152    layout: LayoutItemsResponse,
 153}
 154
 155#[derive(Clone)]
 156enum ListItem {
 157    Unmeasured {
 158        focus_handle: Option<FocusHandle>,
 159    },
 160    Measured {
 161        size: Size<Pixels>,
 162        focus_handle: Option<FocusHandle>,
 163    },
 164}
 165
 166impl ListItem {
 167    fn size(&self) -> Option<Size<Pixels>> {
 168        if let ListItem::Measured { size, .. } = self {
 169            Some(*size)
 170        } else {
 171            None
 172        }
 173    }
 174
 175    fn focus_handle(&self) -> Option<FocusHandle> {
 176        match self {
 177            ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
 178                focus_handle.clone()
 179            }
 180        }
 181    }
 182
 183    fn contains_focused(&self, window: &Window, cx: &App) -> bool {
 184        match self {
 185            ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
 186                focus_handle
 187                    .as_ref()
 188                    .is_some_and(|handle| handle.contains_focused(window, cx))
 189            }
 190        }
 191    }
 192}
 193
 194#[derive(Clone, Debug, Default, PartialEq)]
 195struct ListItemSummary {
 196    count: usize,
 197    rendered_count: usize,
 198    unrendered_count: usize,
 199    height: Pixels,
 200    has_focus_handles: bool,
 201}
 202
 203#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
 204struct Count(usize);
 205
 206#[derive(Clone, Debug, Default)]
 207struct Height(Pixels);
 208
 209impl ListState {
 210    /// Construct a new list state, for storage on a view.
 211    ///
 212    /// The overdraw parameter controls how much extra space is rendered
 213    /// above and below the visible area. Elements within this area will
 214    /// be measured even though they are not visible. This can help ensure
 215    /// that the list doesn't flicker or pop in when scrolling.
 216    pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self {
 217        let this = Self(Rc::new(RefCell::new(StateInner {
 218            last_layout_bounds: None,
 219            last_padding: None,
 220            items: SumTree::default(),
 221            logical_scroll_top: None,
 222            alignment,
 223            overdraw,
 224            scroll_handler: None,
 225            reset: false,
 226            scrollbar_drag_start_height: None,
 227            measuring_behavior: ListMeasuringBehavior::default(),
 228        })));
 229        this.splice(0..0, item_count);
 230        this
 231    }
 232
 233    /// Set the list to measure all items in the list in the first layout phase.
 234    ///
 235    /// This is useful for ensuring that the scrollbar size is correct instead of based on only rendered elements.
 236    pub fn measure_all(self) -> Self {
 237        self.0.borrow_mut().measuring_behavior = ListMeasuringBehavior::Measure(false);
 238        self
 239    }
 240
 241    /// Reset this instantiation of the list state.
 242    ///
 243    /// Note that this will cause scroll events to be dropped until the next paint.
 244    pub fn reset(&self, element_count: usize) {
 245        let old_count = {
 246            let state = &mut *self.0.borrow_mut();
 247            state.reset = true;
 248            state.measuring_behavior.reset();
 249            state.logical_scroll_top = None;
 250            state.scrollbar_drag_start_height = None;
 251            state.items.summary().count
 252        };
 253
 254        self.splice(0..old_count, element_count);
 255    }
 256
 257    /// Remeasure all items without changing scroll position.
 258    ///
 259    /// Use this when item heights may have changed (e.g., font size changes)
 260    /// but the number and identity of items remains the same.
 261    pub fn remeasure(&self) {
 262        let state = &mut *self.0.borrow_mut();
 263        let items = state.items.clone();
 264        state.measuring_behavior.reset();
 265
 266        let new_items = items.cursor::<Count>(()).map(|item| ListItem::Unmeasured {
 267            focus_handle: item.focus_handle(),
 268        });
 269
 270        state.items = SumTree::from_iter(new_items, ());
 271    }
 272
 273    /// The number of items in this list.
 274    pub fn item_count(&self) -> usize {
 275        self.0.borrow().items.summary().count
 276    }
 277
 278    /// Inform the list state that the items in `old_range` have been replaced
 279    /// by `count` new items that must be recalculated.
 280    pub fn splice(&self, old_range: Range<usize>, count: usize) {
 281        self.splice_focusable(old_range, (0..count).map(|_| None))
 282    }
 283
 284    /// Register with the list state that the items in `old_range` have been replaced
 285    /// by new items. As opposed to [`Self::splice`], this method allows an iterator of optional focus handles
 286    /// to be supplied to properly integrate with items in the list that can be focused. If a focused item
 287    /// is scrolled out of view, the list will continue to render it to allow keyboard interaction.
 288    pub fn splice_focusable(
 289        &self,
 290        old_range: Range<usize>,
 291        focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
 292    ) {
 293        let state = &mut *self.0.borrow_mut();
 294
 295        let mut old_items = state.items.cursor::<Count>(());
 296        let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right);
 297        old_items.seek_forward(&Count(old_range.end), Bias::Right);
 298
 299        let mut spliced_count = 0;
 300        new_items.extend(
 301            focus_handles.into_iter().map(|focus_handle| {
 302                spliced_count += 1;
 303                ListItem::Unmeasured { focus_handle }
 304            }),
 305            (),
 306        );
 307        new_items.append(old_items.suffix(), ());
 308        drop(old_items);
 309        state.items = new_items;
 310
 311        if let Some(ListOffset {
 312            item_ix,
 313            offset_in_item,
 314        }) = state.logical_scroll_top.as_mut()
 315        {
 316            if old_range.contains(item_ix) {
 317                *item_ix = old_range.start;
 318                *offset_in_item = px(0.);
 319            } else if old_range.end <= *item_ix {
 320                *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count;
 321            }
 322        }
 323    }
 324
 325    /// Set a handler that will be called when the list is scrolled.
 326    pub fn set_scroll_handler(
 327        &self,
 328        handler: impl FnMut(&ListScrollEvent, &mut Window, &mut App) + 'static,
 329    ) {
 330        self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
 331    }
 332
 333    /// Get the current scroll offset, in terms of the list's items.
 334    pub fn logical_scroll_top(&self) -> ListOffset {
 335        self.0.borrow().logical_scroll_top()
 336    }
 337
 338    /// Scroll the list by the given offset
 339    pub fn scroll_by(&self, distance: Pixels) {
 340        if distance == px(0.) {
 341            return;
 342        }
 343
 344        let current_offset = self.logical_scroll_top();
 345        let state = &mut *self.0.borrow_mut();
 346        let mut cursor = state.items.cursor::<ListItemSummary>(());
 347        cursor.seek(&Count(current_offset.item_ix), Bias::Right);
 348
 349        let start_pixel_offset = cursor.start().height + current_offset.offset_in_item;
 350        let new_pixel_offset = (start_pixel_offset + distance).max(px(0.));
 351        if new_pixel_offset > start_pixel_offset {
 352            cursor.seek_forward(&Height(new_pixel_offset), Bias::Right);
 353        } else {
 354            cursor.seek(&Height(new_pixel_offset), Bias::Right);
 355        }
 356
 357        state.logical_scroll_top = Some(ListOffset {
 358            item_ix: cursor.start().count,
 359            offset_in_item: new_pixel_offset - cursor.start().height,
 360        });
 361    }
 362
 363    /// Scroll the list to the given offset
 364    pub fn scroll_to(&self, mut scroll_top: ListOffset) {
 365        let state = &mut *self.0.borrow_mut();
 366        let item_count = state.items.summary().count;
 367        if scroll_top.item_ix >= item_count {
 368            scroll_top.item_ix = item_count;
 369            scroll_top.offset_in_item = px(0.);
 370        }
 371
 372        state.logical_scroll_top = Some(scroll_top);
 373    }
 374
 375    /// Scroll the list to the given item, such that the item is fully visible.
 376    pub fn scroll_to_reveal_item(&self, ix: usize) {
 377        let state = &mut *self.0.borrow_mut();
 378
 379        let mut scroll_top = state.logical_scroll_top();
 380        let height = state
 381            .last_layout_bounds
 382            .map_or(px(0.), |bounds| bounds.size.height);
 383        let padding = state.last_padding.unwrap_or_default();
 384
 385        if ix <= scroll_top.item_ix {
 386            scroll_top.item_ix = ix;
 387            scroll_top.offset_in_item = px(0.);
 388        } else {
 389            let mut cursor = state.items.cursor::<ListItemSummary>(());
 390            cursor.seek(&Count(ix + 1), Bias::Right);
 391            let bottom = cursor.start().height + padding.top;
 392            let goal_top = px(0.).max(bottom - height + padding.bottom);
 393
 394            cursor.seek(&Height(goal_top), Bias::Left);
 395            let start_ix = cursor.start().count;
 396            let start_item_top = cursor.start().height;
 397
 398            if start_ix >= scroll_top.item_ix {
 399                scroll_top.item_ix = start_ix;
 400                scroll_top.offset_in_item = goal_top - start_item_top;
 401            }
 402        }
 403
 404        state.logical_scroll_top = Some(scroll_top);
 405    }
 406
 407    /// Get the bounds for the given item in window coordinates, if it's
 408    /// been rendered.
 409    pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
 410        let state = &*self.0.borrow();
 411
 412        let bounds = state.last_layout_bounds.unwrap_or_default();
 413        let scroll_top = state.logical_scroll_top();
 414        if ix < scroll_top.item_ix {
 415            return None;
 416        }
 417
 418        let mut cursor = state.items.cursor::<Dimensions<Count, Height>>(());
 419        cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
 420
 421        let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item;
 422
 423        cursor.seek_forward(&Count(ix), Bias::Right);
 424        if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
 425            let &Dimensions(Count(count), Height(top), _) = cursor.start();
 426            if count == ix {
 427                let top = bounds.top() + top - scroll_top;
 428                return Some(Bounds::from_corners(
 429                    point(bounds.left(), top),
 430                    point(bounds.right(), top + size.height),
 431                ));
 432            }
 433        }
 434        None
 435    }
 436
 437    /// Call this method when the user starts dragging the scrollbar.
 438    ///
 439    /// This will prevent the height reported to the scrollbar from changing during the drag
 440    /// as items in the overdraw get measured, and help offset scroll position changes accordingly.
 441    pub fn scrollbar_drag_started(&self) {
 442        let mut state = self.0.borrow_mut();
 443        state.scrollbar_drag_start_height = Some(state.items.summary().height);
 444    }
 445
 446    /// Called when the user stops dragging the scrollbar.
 447    ///
 448    /// See `scrollbar_drag_started`.
 449    pub fn scrollbar_drag_ended(&self) {
 450        self.0.borrow_mut().scrollbar_drag_start_height.take();
 451    }
 452
 453    /// Set the offset from the scrollbar
 454    pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
 455        self.0.borrow_mut().set_offset_from_scrollbar(point);
 456    }
 457
 458    /// Returns the maximum scroll offset according to the items we have measured.
 459    /// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly.
 460    pub fn max_offset_for_scrollbar(&self) -> Size<Pixels> {
 461        let state = self.0.borrow();
 462        let bounds = state.last_layout_bounds.unwrap_or_default();
 463
 464        let height = state
 465            .scrollbar_drag_start_height
 466            .unwrap_or_else(|| state.items.summary().height);
 467
 468        Size::new(Pixels::ZERO, Pixels::ZERO.max(height - bounds.size.height))
 469    }
 470
 471    /// Returns the current scroll offset adjusted for the scrollbar
 472    pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
 473        let state = &self.0.borrow();
 474        let logical_scroll_top = state.logical_scroll_top();
 475
 476        let mut cursor = state.items.cursor::<ListItemSummary>(());
 477        let summary: ListItemSummary =
 478            cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right);
 479        let content_height = state.items.summary().height;
 480        let drag_offset =
 481            // if dragging the scrollbar, we want to offset the point if the height changed
 482            content_height - state.scrollbar_drag_start_height.unwrap_or(content_height);
 483        let offset = summary.height + logical_scroll_top.offset_in_item - drag_offset;
 484
 485        Point::new(px(0.), -offset)
 486    }
 487
 488    /// Return the bounds of the viewport in pixels.
 489    pub fn viewport_bounds(&self) -> Bounds<Pixels> {
 490        self.0.borrow().last_layout_bounds.unwrap_or_default()
 491    }
 492}
 493
 494impl StateInner {
 495    fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
 496        let mut cursor = self.items.cursor::<ListItemSummary>(());
 497        cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
 498        let start_y = cursor.start().height + scroll_top.offset_in_item;
 499        cursor.seek_forward(&Height(start_y + height), Bias::Left);
 500        scroll_top.item_ix..cursor.start().count + 1
 501    }
 502
 503    fn scroll(
 504        &mut self,
 505        scroll_top: &ListOffset,
 506        height: Pixels,
 507        delta: Point<Pixels>,
 508        current_view: EntityId,
 509        window: &mut Window,
 510        cx: &mut App,
 511    ) {
 512        // Drop scroll events after a reset, since we can't calculate
 513        // the new logical scroll top without the item heights
 514        if self.reset {
 515            return;
 516        }
 517
 518        let padding = self.last_padding.unwrap_or_default();
 519        let scroll_max =
 520            (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
 521        let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
 522            .max(px(0.))
 523            .min(scroll_max);
 524
 525        if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
 526            self.logical_scroll_top = None;
 527        } else {
 528            let (start, ..) =
 529                self.items
 530                    .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
 531            let item_ix = start.count;
 532            let offset_in_item = new_scroll_top - start.height;
 533            self.logical_scroll_top = Some(ListOffset {
 534                item_ix,
 535                offset_in_item,
 536            });
 537        }
 538
 539        if self.scroll_handler.is_some() {
 540            let visible_range = self.visible_range(height, scroll_top);
 541            self.scroll_handler.as_mut().unwrap()(
 542                &ListScrollEvent {
 543                    visible_range,
 544                    count: self.items.summary().count,
 545                    is_scrolled: self.logical_scroll_top.is_some(),
 546                },
 547                window,
 548                cx,
 549            );
 550        }
 551
 552        cx.notify(current_view);
 553    }
 554
 555    fn logical_scroll_top(&self) -> ListOffset {
 556        self.logical_scroll_top
 557            .unwrap_or_else(|| match self.alignment {
 558                ListAlignment::Top => ListOffset {
 559                    item_ix: 0,
 560                    offset_in_item: px(0.),
 561                },
 562                ListAlignment::Bottom => ListOffset {
 563                    item_ix: self.items.summary().count,
 564                    offset_in_item: px(0.),
 565                },
 566            })
 567    }
 568
 569    fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
 570        let (start, ..) = self.items.find::<ListItemSummary, _>(
 571            (),
 572            &Count(logical_scroll_top.item_ix),
 573            Bias::Right,
 574        );
 575        start.height + logical_scroll_top.offset_in_item
 576    }
 577
 578    fn layout_all_items(
 579        &mut self,
 580        available_width: Pixels,
 581        render_item: &mut RenderItemFn,
 582        window: &mut Window,
 583        cx: &mut App,
 584    ) {
 585        match &mut self.measuring_behavior {
 586            ListMeasuringBehavior::Visible => {
 587                return;
 588            }
 589            ListMeasuringBehavior::Measure(has_measured) => {
 590                if *has_measured {
 591                    return;
 592                }
 593                *has_measured = true;
 594            }
 595        }
 596
 597        let mut cursor = self.items.cursor::<Count>(());
 598        let available_item_space = size(
 599            AvailableSpace::Definite(available_width),
 600            AvailableSpace::MinContent,
 601        );
 602
 603        let mut measured_items = Vec::default();
 604
 605        for (ix, item) in cursor.enumerate() {
 606            let size = item.size().unwrap_or_else(|| {
 607                let mut element = render_item(ix, window, cx);
 608                element.layout_as_root(available_item_space, window, cx)
 609            });
 610
 611            measured_items.push(ListItem::Measured {
 612                size,
 613                focus_handle: item.focus_handle(),
 614            });
 615        }
 616
 617        self.items = SumTree::from_iter(measured_items, ());
 618    }
 619
 620    fn layout_items(
 621        &mut self,
 622        available_width: Option<Pixels>,
 623        available_height: Pixels,
 624        padding: &Edges<Pixels>,
 625        render_item: &mut RenderItemFn,
 626        window: &mut Window,
 627        cx: &mut App,
 628    ) -> LayoutItemsResponse {
 629        let old_items = self.items.clone();
 630        let mut measured_items = VecDeque::new();
 631        let mut item_layouts = VecDeque::new();
 632        let mut rendered_height = padding.top;
 633        let mut max_item_width = px(0.);
 634        let mut scroll_top = self.logical_scroll_top();
 635        let mut rendered_focused_item = false;
 636
 637        let available_item_space = size(
 638            available_width.map_or(AvailableSpace::MinContent, |width| {
 639                AvailableSpace::Definite(width)
 640            }),
 641            AvailableSpace::MinContent,
 642        );
 643
 644        let mut cursor = old_items.cursor::<Count>(());
 645
 646        // Render items after the scroll top, including those in the trailing overdraw
 647        cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
 648        for (ix, item) in cursor.by_ref().enumerate() {
 649            let visible_height = rendered_height - scroll_top.offset_in_item;
 650            if visible_height >= available_height + self.overdraw {
 651                break;
 652            }
 653
 654            // Use the previously cached height and focus handle if available
 655            let mut size = item.size();
 656
 657            // If we're within the visible area or the height wasn't cached, render and measure the item's element
 658            if visible_height < available_height || size.is_none() {
 659                let item_index = scroll_top.item_ix + ix;
 660                let mut element = render_item(item_index, window, cx);
 661                let element_size = element.layout_as_root(available_item_space, window, cx);
 662                size = Some(element_size);
 663                if visible_height < available_height {
 664                    item_layouts.push_back(ItemLayout {
 665                        index: item_index,
 666                        element,
 667                        size: element_size,
 668                    });
 669                    if item.contains_focused(window, cx) {
 670                        rendered_focused_item = true;
 671                    }
 672                }
 673            }
 674
 675            let size = size.unwrap();
 676            rendered_height += size.height;
 677            max_item_width = max_item_width.max(size.width);
 678            measured_items.push_back(ListItem::Measured {
 679                size,
 680                focus_handle: item.focus_handle(),
 681            });
 682        }
 683        rendered_height += padding.bottom;
 684
 685        // Prepare to start walking upward from the item at the scroll top.
 686        cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
 687
 688        // If the rendered items do not fill the visible region, then adjust
 689        // the scroll top upward.
 690        if rendered_height - scroll_top.offset_in_item < available_height {
 691            while rendered_height < available_height {
 692                cursor.prev();
 693                if let Some(item) = cursor.item() {
 694                    let item_index = cursor.start().0;
 695                    let mut element = render_item(item_index, window, cx);
 696                    let element_size = element.layout_as_root(available_item_space, window, cx);
 697                    let focus_handle = item.focus_handle();
 698                    rendered_height += element_size.height;
 699                    measured_items.push_front(ListItem::Measured {
 700                        size: element_size,
 701                        focus_handle,
 702                    });
 703                    item_layouts.push_front(ItemLayout {
 704                        index: item_index,
 705                        element,
 706                        size: element_size,
 707                    });
 708                    if item.contains_focused(window, cx) {
 709                        rendered_focused_item = true;
 710                    }
 711                } else {
 712                    break;
 713                }
 714            }
 715
 716            scroll_top = ListOffset {
 717                item_ix: cursor.start().0,
 718                offset_in_item: rendered_height - available_height,
 719            };
 720
 721            match self.alignment {
 722                ListAlignment::Top => {
 723                    scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
 724                    self.logical_scroll_top = Some(scroll_top);
 725                }
 726                ListAlignment::Bottom => {
 727                    scroll_top = ListOffset {
 728                        item_ix: cursor.start().0,
 729                        offset_in_item: rendered_height - available_height,
 730                    };
 731                    self.logical_scroll_top = None;
 732                }
 733            };
 734        }
 735
 736        // Measure items in the leading overdraw
 737        let mut leading_overdraw = scroll_top.offset_in_item;
 738        while leading_overdraw < self.overdraw {
 739            cursor.prev();
 740            if let Some(item) = cursor.item() {
 741                let size = if let ListItem::Measured { size, .. } = item {
 742                    *size
 743                } else {
 744                    let mut element = render_item(cursor.start().0, window, cx);
 745                    element.layout_as_root(available_item_space, window, cx)
 746                };
 747
 748                leading_overdraw += size.height;
 749                measured_items.push_front(ListItem::Measured {
 750                    size,
 751                    focus_handle: item.focus_handle(),
 752                });
 753            } else {
 754                break;
 755            }
 756        }
 757
 758        let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
 759        let mut cursor = old_items.cursor::<Count>(());
 760        let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right);
 761        new_items.extend(measured_items, ());
 762        cursor.seek(&Count(measured_range.end), Bias::Right);
 763        new_items.append(cursor.suffix(), ());
 764        self.items = new_items;
 765
 766        // If none of the visible items are focused, check if an off-screen item is focused
 767        // and include it to be rendered after the visible items so keyboard interaction continues
 768        // to work for it.
 769        if !rendered_focused_item {
 770            let mut cursor = self
 771                .items
 772                .filter::<_, Count>((), |summary| summary.has_focus_handles);
 773            cursor.next();
 774            while let Some(item) = cursor.item() {
 775                if item.contains_focused(window, cx) {
 776                    let item_index = cursor.start().0;
 777                    let mut element = render_item(cursor.start().0, window, cx);
 778                    let size = element.layout_as_root(available_item_space, window, cx);
 779                    item_layouts.push_back(ItemLayout {
 780                        index: item_index,
 781                        element,
 782                        size,
 783                    });
 784                    break;
 785                }
 786                cursor.next();
 787            }
 788        }
 789
 790        LayoutItemsResponse {
 791            max_item_width,
 792            scroll_top,
 793            item_layouts,
 794        }
 795    }
 796
 797    fn prepaint_items(
 798        &mut self,
 799        bounds: Bounds<Pixels>,
 800        padding: Edges<Pixels>,
 801        autoscroll: bool,
 802        render_item: &mut RenderItemFn,
 803        window: &mut Window,
 804        cx: &mut App,
 805    ) -> Result<LayoutItemsResponse, ListOffset> {
 806        window.transact(|window| {
 807            match self.measuring_behavior {
 808                ListMeasuringBehavior::Measure(has_measured) if !has_measured => {
 809                    self.layout_all_items(bounds.size.width, render_item, window, cx);
 810                }
 811                _ => {}
 812            }
 813
 814            let mut layout_response = self.layout_items(
 815                Some(bounds.size.width),
 816                bounds.size.height,
 817                &padding,
 818                render_item,
 819                window,
 820                cx,
 821            );
 822
 823            // Avoid honoring autoscroll requests from elements other than our children.
 824            window.take_autoscroll();
 825
 826            // Only paint the visible items, if there is actually any space for them (taking padding into account)
 827            if bounds.size.height > padding.top + padding.bottom {
 828                let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
 829                item_origin.y -= layout_response.scroll_top.offset_in_item;
 830                for item in &mut layout_response.item_layouts {
 831                    window.with_content_mask(Some(ContentMask { bounds }), |window| {
 832                        item.element.prepaint_at(item_origin, window, cx);
 833                    });
 834
 835                    if let Some(autoscroll_bounds) = window.take_autoscroll()
 836                        && autoscroll
 837                    {
 838                        if autoscroll_bounds.top() < bounds.top() {
 839                            return Err(ListOffset {
 840                                item_ix: item.index,
 841                                offset_in_item: autoscroll_bounds.top() - item_origin.y,
 842                            });
 843                        } else if autoscroll_bounds.bottom() > bounds.bottom() {
 844                            let mut cursor = self.items.cursor::<Count>(());
 845                            cursor.seek(&Count(item.index), Bias::Right);
 846                            let mut height = bounds.size.height - padding.top - padding.bottom;
 847
 848                            // Account for the height of the element down until the autoscroll bottom.
 849                            height -= autoscroll_bounds.bottom() - item_origin.y;
 850
 851                            // Keep decreasing the scroll top until we fill all the available space.
 852                            while height > Pixels::ZERO {
 853                                cursor.prev();
 854                                let Some(item) = cursor.item() else { break };
 855
 856                                let size = item.size().unwrap_or_else(|| {
 857                                    let mut item = render_item(cursor.start().0, window, cx);
 858                                    let item_available_size =
 859                                        size(bounds.size.width.into(), AvailableSpace::MinContent);
 860                                    item.layout_as_root(item_available_size, window, cx)
 861                                });
 862                                height -= size.height;
 863                            }
 864
 865                            return Err(ListOffset {
 866                                item_ix: cursor.start().0,
 867                                offset_in_item: if height < Pixels::ZERO {
 868                                    -height
 869                                } else {
 870                                    Pixels::ZERO
 871                                },
 872                            });
 873                        }
 874                    }
 875
 876                    item_origin.y += item.size.height;
 877                }
 878            } else {
 879                layout_response.item_layouts.clear();
 880            }
 881
 882            Ok(layout_response)
 883        })
 884    }
 885
 886    // Scrollbar support
 887
 888    fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
 889        let Some(bounds) = self.last_layout_bounds else {
 890            return;
 891        };
 892        let height = bounds.size.height;
 893
 894        let padding = self.last_padding.unwrap_or_default();
 895        let content_height = self.items.summary().height;
 896        let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
 897        let drag_offset =
 898            // if dragging the scrollbar, we want to offset the point if the height changed
 899            content_height - self.scrollbar_drag_start_height.unwrap_or(content_height);
 900        let new_scroll_top = (point.y - drag_offset).abs().max(px(0.)).min(scroll_max);
 901
 902        if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
 903            self.logical_scroll_top = None;
 904        } else {
 905            let (start, _, _) =
 906                self.items
 907                    .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
 908
 909            let item_ix = start.count;
 910            let offset_in_item = new_scroll_top - start.height;
 911            self.logical_scroll_top = Some(ListOffset {
 912                item_ix,
 913                offset_in_item,
 914            });
 915        }
 916    }
 917}
 918
 919impl std::fmt::Debug for ListItem {
 920    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 921        match self {
 922            Self::Unmeasured { .. } => write!(f, "Unrendered"),
 923            Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
 924        }
 925    }
 926}
 927
 928/// An offset into the list's items, in terms of the item index and the number
 929/// of pixels off the top left of the item.
 930#[derive(Debug, Clone, Copy, Default)]
 931pub struct ListOffset {
 932    /// The index of an item in the list
 933    pub item_ix: usize,
 934    /// The number of pixels to offset from the item index.
 935    pub offset_in_item: Pixels,
 936}
 937
 938impl Element for List {
 939    type RequestLayoutState = ();
 940    type PrepaintState = ListPrepaintState;
 941
 942    fn id(&self) -> Option<crate::ElementId> {
 943        None
 944    }
 945
 946    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
 947        None
 948    }
 949
 950    fn request_layout(
 951        &mut self,
 952        _id: Option<&GlobalElementId>,
 953        _inspector_id: Option<&InspectorElementId>,
 954        window: &mut Window,
 955        cx: &mut App,
 956    ) -> (crate::LayoutId, Self::RequestLayoutState) {
 957        let layout_id = match self.sizing_behavior {
 958            ListSizingBehavior::Infer => {
 959                let mut style = Style::default();
 960                style.overflow.y = Overflow::Scroll;
 961                style.refine(&self.style);
 962                window.with_text_style(style.text_style().cloned(), |window| {
 963                    let state = &mut *self.state.0.borrow_mut();
 964
 965                    let available_height = if let Some(last_bounds) = state.last_layout_bounds {
 966                        last_bounds.size.height
 967                    } else {
 968                        // If we don't have the last layout bounds (first render),
 969                        // we might just use the overdraw value as the available height to layout enough items.
 970                        state.overdraw
 971                    };
 972                    let padding = style.padding.to_pixels(
 973                        state.last_layout_bounds.unwrap_or_default().size.into(),
 974                        window.rem_size(),
 975                    );
 976
 977                    let layout_response = state.layout_items(
 978                        None,
 979                        available_height,
 980                        &padding,
 981                        &mut self.render_item,
 982                        window,
 983                        cx,
 984                    );
 985                    let max_element_width = layout_response.max_item_width;
 986
 987                    let summary = state.items.summary();
 988                    let total_height = summary.height;
 989
 990                    window.request_measured_layout(
 991                        style,
 992                        move |known_dimensions, available_space, _window, _cx| {
 993                            let width =
 994                                known_dimensions
 995                                    .width
 996                                    .unwrap_or(match available_space.width {
 997                                        AvailableSpace::Definite(x) => x,
 998                                        AvailableSpace::MinContent | AvailableSpace::MaxContent => {
 999                                            max_element_width
1000                                        }
1001                                    });
1002                            let height = match available_space.height {
1003                                AvailableSpace::Definite(height) => total_height.min(height),
1004                                AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1005                                    total_height
1006                                }
1007                            };
1008                            size(width, height)
1009                        },
1010                    )
1011                })
1012            }
1013            ListSizingBehavior::Auto => {
1014                let mut style = Style::default();
1015                style.refine(&self.style);
1016                window.with_text_style(style.text_style().cloned(), |window| {
1017                    window.request_layout(style, None, cx)
1018                })
1019            }
1020        };
1021        (layout_id, ())
1022    }
1023
1024    fn prepaint(
1025        &mut self,
1026        _id: Option<&GlobalElementId>,
1027        _inspector_id: Option<&InspectorElementId>,
1028        bounds: Bounds<Pixels>,
1029        _: &mut Self::RequestLayoutState,
1030        window: &mut Window,
1031        cx: &mut App,
1032    ) -> ListPrepaintState {
1033        let state = &mut *self.state.0.borrow_mut();
1034        state.reset = false;
1035
1036        let mut style = Style::default();
1037        style.refine(&self.style);
1038
1039        let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1040
1041        // If the width of the list has changed, invalidate all cached item heights
1042        if state
1043            .last_layout_bounds
1044            .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width)
1045        {
1046            let new_items = SumTree::from_iter(
1047                state.items.iter().map(|item| ListItem::Unmeasured {
1048                    focus_handle: item.focus_handle(),
1049                }),
1050                (),
1051            );
1052
1053            state.items = new_items;
1054        }
1055
1056        let padding = style
1057            .padding
1058            .to_pixels(bounds.size.into(), window.rem_size());
1059        let layout =
1060            match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) {
1061                Ok(layout) => layout,
1062                Err(autoscroll_request) => {
1063                    state.logical_scroll_top = Some(autoscroll_request);
1064                    state
1065                        .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx)
1066                        .unwrap()
1067                }
1068            };
1069
1070        state.last_layout_bounds = Some(bounds);
1071        state.last_padding = Some(padding);
1072        ListPrepaintState { hitbox, layout }
1073    }
1074
1075    fn paint(
1076        &mut self,
1077        _id: Option<&GlobalElementId>,
1078        _inspector_id: Option<&InspectorElementId>,
1079        bounds: Bounds<crate::Pixels>,
1080        _: &mut Self::RequestLayoutState,
1081        prepaint: &mut Self::PrepaintState,
1082        window: &mut Window,
1083        cx: &mut App,
1084    ) {
1085        let current_view = window.current_view();
1086        window.with_content_mask(Some(ContentMask { bounds }), |window| {
1087            for item in &mut prepaint.layout.item_layouts {
1088                item.element.paint(window, cx);
1089            }
1090        });
1091
1092        let list_state = self.state.clone();
1093        let height = bounds.size.height;
1094        let scroll_top = prepaint.layout.scroll_top;
1095        let hitbox_id = prepaint.hitbox.id;
1096        let mut accumulated_scroll_delta = ScrollDelta::default();
1097        window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
1098            if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) {
1099                accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
1100                let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
1101                list_state.0.borrow_mut().scroll(
1102                    &scroll_top,
1103                    height,
1104                    pixel_delta,
1105                    current_view,
1106                    window,
1107                    cx,
1108                )
1109            }
1110        });
1111    }
1112}
1113
1114impl IntoElement for List {
1115    type Element = Self;
1116
1117    fn into_element(self) -> Self::Element {
1118        self
1119    }
1120}
1121
1122impl Styled for List {
1123    fn style(&mut self) -> &mut StyleRefinement {
1124        &mut self.style
1125    }
1126}
1127
1128impl sum_tree::Item for ListItem {
1129    type Summary = ListItemSummary;
1130
1131    fn summary(&self, _: ()) -> Self::Summary {
1132        match self {
1133            ListItem::Unmeasured { focus_handle } => ListItemSummary {
1134                count: 1,
1135                rendered_count: 0,
1136                unrendered_count: 1,
1137                height: px(0.),
1138                has_focus_handles: focus_handle.is_some(),
1139            },
1140            ListItem::Measured {
1141                size, focus_handle, ..
1142            } => ListItemSummary {
1143                count: 1,
1144                rendered_count: 1,
1145                unrendered_count: 0,
1146                height: size.height,
1147                has_focus_handles: focus_handle.is_some(),
1148            },
1149        }
1150    }
1151}
1152
1153impl sum_tree::ContextLessSummary for ListItemSummary {
1154    fn zero() -> Self {
1155        Default::default()
1156    }
1157
1158    fn add_summary(&mut self, summary: &Self) {
1159        self.count += summary.count;
1160        self.rendered_count += summary.rendered_count;
1161        self.unrendered_count += summary.unrendered_count;
1162        self.height += summary.height;
1163        self.has_focus_handles |= summary.has_focus_handles;
1164    }
1165}
1166
1167impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
1168    fn zero(_cx: ()) -> Self {
1169        Default::default()
1170    }
1171
1172    fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1173        self.0 += summary.count;
1174    }
1175}
1176
1177impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
1178    fn zero(_cx: ()) -> Self {
1179        Default::default()
1180    }
1181
1182    fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1183        self.0 += summary.height;
1184    }
1185}
1186
1187impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Count {
1188    fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1189        self.0.partial_cmp(&other.count).unwrap()
1190    }
1191}
1192
1193impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height {
1194    fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1195        self.0.partial_cmp(&other.height).unwrap()
1196    }
1197}
1198
1199#[cfg(test)]
1200mod test {
1201
1202    use gpui::{ScrollDelta, ScrollWheelEvent};
1203
1204    use crate::{self as gpui, TestAppContext};
1205
1206    #[gpui::test]
1207    fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
1208        use crate::{
1209            AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
1210            list, point, px, size,
1211        };
1212
1213        let cx = cx.add_empty_window();
1214
1215        let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1216
1217        // Ensure that the list is scrolled to the top
1218        state.scroll_to(gpui::ListOffset {
1219            item_ix: 0,
1220            offset_in_item: px(0.0),
1221        });
1222
1223        struct TestView(ListState);
1224        impl Render for TestView {
1225            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1226                list(self.0.clone(), |_, _, _| {
1227                    div().h(px(10.)).w_full().into_any()
1228                })
1229                .w_full()
1230                .h_full()
1231            }
1232        }
1233
1234        // Paint
1235        cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1236            cx.new(|_| TestView(state.clone()))
1237        });
1238
1239        // Reset
1240        state.reset(5);
1241
1242        // And then receive a scroll event _before_ the next paint
1243        cx.simulate_event(ScrollWheelEvent {
1244            position: point(px(1.), px(1.)),
1245            delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
1246            ..Default::default()
1247        });
1248
1249        // Scroll position should stay at the top of the list
1250        assert_eq!(state.logical_scroll_top().item_ix, 0);
1251        assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
1252    }
1253
1254    #[gpui::test]
1255    fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) {
1256        use crate::{
1257            AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
1258            list, point, px, size,
1259        };
1260
1261        let cx = cx.add_empty_window();
1262
1263        let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1264
1265        struct TestView(ListState);
1266        impl Render for TestView {
1267            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1268                list(self.0.clone(), |_, _, _| {
1269                    div().h(px(20.)).w_full().into_any()
1270                })
1271                .w_full()
1272                .h_full()
1273            }
1274        }
1275
1276        // Paint
1277        cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
1278            cx.new(|_| TestView(state.clone()))
1279        });
1280
1281        // Test positive distance: start at item 1, move down 30px
1282        state.scroll_by(px(30.));
1283
1284        // Should move to item 2
1285        let offset = state.logical_scroll_top();
1286        assert_eq!(offset.item_ix, 1);
1287        assert_eq!(offset.offset_in_item, px(10.));
1288
1289        // Test negative distance: start at item 2, move up 30px
1290        state.scroll_by(px(-30.));
1291
1292        // Should move back to item 1
1293        let offset = state.logical_scroll_top();
1294        assert_eq!(offset.item_ix, 0);
1295        assert_eq!(offset.offset_in_item, px(0.));
1296
1297        // Test zero distance
1298        state.scroll_by(px(0.));
1299        let offset = state.logical_scroll_top();
1300        assert_eq!(offset.item_ix, 0);
1301        assert_eq!(offset.offset_in_item, px(0.));
1302    }
1303}