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