2789 lines · 101.7 KB · Rust
RawDownload
//! A list element that can be used to render a large number of differently sized elements//! efficiently. Clients of this API need to ensure that elements outside of the scrolled//! area do not change their height for this element to function correctly. If your elements//! do change height, notify the list element via [`ListState::splice`] or [`ListState::reset`].//! In order to minimize re-renders, this element's state is stored intrusively//! on your own views, so that your code can coordinate directly with the list element's cached state.//!//! If all of your elements are the same height, see [`crate::UniformList`] for a simpler API use crate::{    AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,    FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,    Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,    Window, point, px, size,};use collections::VecDeque;use refineable::Refineable as _;use std::{cell::RefCell, ops::Range, rc::Rc};use sum_tree::{Bias, Dimensions, SumTree}; type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static; /// Construct a new list elementpub fn list(    state: ListState,    render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static,) -> List {    List {        state,        render_item: Box::new(render_item),        style: StyleRefinement::default(),        sizing_behavior: ListSizingBehavior::default(),    }} /// A list elementpub struct List {    state: ListState,    render_item: Box<RenderItemFn>,    style: StyleRefinement,    sizing_behavior: ListSizingBehavior,} impl List {    /// Set the sizing behavior for the list.    pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {        self.sizing_behavior = behavior;        self    }} /// The list state that views must hold on behalf of the list element.#[derive(Clone)]pub struct ListState(Rc<RefCell<StateInner>>); impl std::fmt::Debug for ListState {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        f.write_str("ListState")    }} struct StateInner {    last_layout_bounds: Option<Bounds<Pixels>>,    last_padding: Option<Edges<Pixels>>,    items: SumTree<ListItem>,    logical_scroll_top: Option<ListOffset>,    alignment: ListAlignment,    overdraw: Pixels,    reset: bool,    #[allow(clippy::type_complexity)]    scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,    scrollbar_drag_start_height: Option<Pixels>,    measuring_behavior: ListMeasuringBehavior,    pending_scroll: Option<PendingScroll>,    follow_state: FollowState,} /// Deferred scroll adjustment applied after the scroll-top item has been remeasured.////// An absolute pending scroll preserves the same pixel offset into the item, which keeps/// visible text stable while content is appended to or removed from that item. A/// proportional pending scroll preserves the same fractional position within the item,/// which is useful when the whole list is being resized and each item scales similarly.#[derive(Clone)]enum PendingScroll {    /// Preserve the same pixel offset into the item after it is remeasured.    Absolute { item_ix: usize, offset: Pixels },    /// Preserve the same fractional offset into the item after it is remeasured.    Proportional(PendingScrollFraction),} /// Keeps track of a fractional scroll position within an item for restoration/// after remeasurement.#[derive(Clone)]struct PendingScrollFraction {    /// The index of the item to scroll within.    item_ix: usize,    /// Fractional offset (0.0 to 1.0) within the item's height.    fraction: f32,} /// Determines how remeasurement preserves the scroll position when the scroll-top item/// changes height.enum ScrollAnchor {    /// Preserve the same pixel offset into the scroll-top item.    Absolute,    /// Preserve the same fractional position within the scroll-top item.    Proportional,} /// Controls whether the list automatically follows new content at the end.#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]pub enum FollowMode {    /// Normal scrolling — no automatic following.    #[default]    Normal,    /// The list should auto-scroll along with the tail, when scrolled to bottom.    Tail,} #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]enum FollowState {    #[default]    Normal,    Tail {        is_following: bool,    },} impl FollowState {    fn is_following(&self) -> bool {        matches!(self, FollowState::Tail { is_following: true })    }     fn has_stopped_following(&self) -> bool {        matches!(            self,            FollowState::Tail {                is_following: false            }        )    }     fn start_following(&mut self) {        if let FollowState::Tail {            is_following: false,        } = self        {            *self = FollowState::Tail { is_following: true };        }    }     fn stop_following(&mut self) {        if let FollowState::Tail { is_following: true } = self {            *self = FollowState::Tail {                is_following: false,            };        }    }} /// Whether the list is scrolling from top to bottom or bottom to top.#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub enum ListAlignment {    /// The list is scrolling from top to bottom, like most lists.    Top,    /// The list is scrolling from bottom to top, like a chat log.    Bottom,} /// A scroll event that has been converted to be in terms of the list's items.pub struct ListScrollEvent {    /// The range of items currently visible in the list, after applying the scroll event.    pub visible_range: Range<usize>,     /// The number of items that are currently visible in the list, after applying the scroll event.    pub count: usize,     /// Whether the list has been scrolled.    pub is_scrolled: bool,     /// Whether the list is currently in follow-tail mode (auto-scrolling to end).    pub is_following_tail: bool,} /// The sizing behavior to apply during layout.#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]pub enum ListSizingBehavior {    /// The list should calculate its size based on the size of its items.    Infer,    /// The list should not calculate a fixed size.    #[default]    Auto,} /// The measuring behavior to apply during layout.#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]pub enum ListMeasuringBehavior {    /// Measure all items in the list.    /// Note: This can be expensive for the first frame in a large list.    Measure(bool),    /// Only measure visible items    #[default]    Visible,} impl ListMeasuringBehavior {    fn reset(&mut self) {        match self {            ListMeasuringBehavior::Measure(has_measured) => *has_measured = false,            ListMeasuringBehavior::Visible => {}        }    }} /// The horizontal sizing behavior to apply during layout.#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]pub enum ListHorizontalSizingBehavior {    /// List items' width can never exceed the width of the list.    #[default]    FitList,    /// List items' width may go over the width of the list, if any item is wider.    Unconstrained,} struct LayoutItemsResponse {    max_item_width: Pixels,    scroll_top: ListOffset,    item_layouts: VecDeque<ItemLayout>,} struct ItemLayout {    index: usize,    element: AnyElement,    size: Size<Pixels>,} /// Frame state used by the [List] element after layout.pub struct ListPrepaintState {    hitbox: Hitbox,    layout: LayoutItemsResponse,} #[derive(Clone)]enum ListItem {    Unmeasured {        size_hint: Option<Size<Pixels>>,        focus_handle: Option<FocusHandle>,    },    Measured {        size: Size<Pixels>,        focus_handle: Option<FocusHandle>,    },} impl ListItem {    fn size(&self) -> Option<Size<Pixels>> {        if let ListItem::Measured { size, .. } = self {            Some(*size)        } else {            None        }    }     fn size_hint(&self) -> Option<Size<Pixels>> {        match self {            ListItem::Measured { size, .. } => Some(*size),            ListItem::Unmeasured { size_hint, .. } => *size_hint,        }    }     fn focus_handle(&self) -> Option<FocusHandle> {        match self {            ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => {                focus_handle.clone()            }        }    }     fn contains_focused(&self, window: &Window, cx: &App) -> bool {        match self {            ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => {                focus_handle                    .as_ref()                    .is_some_and(|handle| handle.contains_focused(window, cx))            }        }    }} #[derive(Clone, Debug, Default, PartialEq)]struct ListItemSummary {    count: usize,    rendered_count: usize,    unrendered_count: usize,    height: Pixels,    has_focus_handles: bool,    has_unknown_height: bool,} #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]struct Count(usize); #[derive(Clone, Debug, Default)]struct Height(Pixels); impl ListState {    /// Construct a new list state, for storage on a view.    ///    /// The overdraw parameter controls how much extra space is rendered    /// above and below the visible area. Elements within this area will    /// be measured even though they are not visible. This can help ensure    /// that the list doesn't flicker or pop in when scrolling.    pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self {        let this = Self(Rc::new(RefCell::new(StateInner {            last_layout_bounds: None,            last_padding: None,            items: SumTree::default(),            logical_scroll_top: None,            alignment,            overdraw,            scroll_handler: None,            reset: false,            scrollbar_drag_start_height: None,            measuring_behavior: ListMeasuringBehavior::default(),            pending_scroll: None,            follow_state: FollowState::default(),        })));        this.splice(0..0, item_count);        this    }     /// Set the list to measure all items in the list in the first layout phase.    ///    /// This is useful for ensuring that the scrollbar size is correct instead of based on only rendered elements.    pub fn measure_all(self) -> Self {        self.0.borrow_mut().measuring_behavior = ListMeasuringBehavior::Measure(false);        self    }     /// Pre-populate every unmeasured item with a uniform height hint so the scrollbar thumb    /// is correctly sized from the first frame, without measuring all items up front.    ///    /// As items are actually rendered their real heights replace the hint, so the scrollbar    /// converges to the exact size over time. This is a cheaper alternative to [`Self::measure_all`]    /// for lists where items have roughly uniform heights (e.g. table rows).    pub fn with_uniform_item_height(self, height: Pixels) -> Self {        self.apply_uniform_item_height(height);        self    }     /// Reset this instantiation of the list state.    ///    /// Note that this will cause scroll events to be dropped until the next paint.    pub fn reset(&self, element_count: usize) {        let old_count = {            let state = &mut *self.0.borrow_mut();            state.reset = true;            state.measuring_behavior.reset();            state.logical_scroll_top = None;            state.pending_scroll = None;            state.scrollbar_drag_start_height = None;            state.items.summary().count        };         self.splice(0..old_count, element_count);    }     /// Reset the list to `element_count` items, pre-populating every item with a    /// uniform height hint so the scrollbar thumb is correctly sized from the first    /// frame even for off-screen items.    pub fn reset_with_uniform_height(&self, element_count: usize, height: Pixels) {        self.reset(element_count);        self.apply_uniform_item_height(height);    }     fn apply_uniform_item_height(&self, height: Pixels) {        let size_hint = Size {            width: px(0.),            height,        };        let mut state = self.0.borrow_mut();        let new_items = state            .items            .iter()            .map(|item| ListItem::Unmeasured {                size_hint: Some(item.size_hint().unwrap_or(size_hint)),                focus_handle: item.focus_handle(),            })            .collect::<Vec<_>>();        let mut tree = SumTree::default();        tree.extend(new_items, ());        state.items = tree;    }     /// Remeasure all items while preserving proportional scroll position.    ///    /// Use this when item heights may have changed (e.g., font size changes)    /// but the number and identity of items remains the same.    pub fn remeasure(&self) {        let count = self.item_count();        self.remeasure_items_with_scroll_anchor(0..count, ScrollAnchor::Proportional);    }     /// Mark items in `range` as needing remeasurement while preserving    /// the current scroll position. Unlike [`Self::splice`], this does    /// not change the number of items or blow away `logical_scroll_top`.    ///    /// Use this when an item's content has changed and its rendered    /// height may be different (e.g., streaming text, tool results    /// loading), but the item itself still exists at the same index.    pub fn remeasure_items(&self, range: Range<usize>) {        self.remeasure_items_with_scroll_anchor(range, ScrollAnchor::Absolute);    }     fn remeasure_items_with_scroll_anchor(&self, range: Range<usize>, scroll_anchor: ScrollAnchor) {        let state = &mut *self.0.borrow_mut();         if let Some(scroll_top) = state.logical_scroll_top {            if range.contains(&scroll_top.item_ix) {                state.pending_scroll = match scroll_anchor {                    ScrollAnchor::Absolute => Some(PendingScroll::Absolute {                        item_ix: scroll_top.item_ix,                        offset: scroll_top.offset_in_item,                    }),                    ScrollAnchor::Proportional => {                        // If the scroll-top item falls within the remeasured range,                        // store a fractional offset so the layout can restore the                        // proportional scroll position after the item is re-rendered                        // at its new height.                        let mut cursor = state.items.cursor::<Count>(());                        cursor.seek(&Count(scroll_top.item_ix), Bias::Right);                         cursor                            .item()                            .and_then(|item| {                                item.size().map(|size| {                                    let fraction = if size.height.0 > 0.0 {                                        (scroll_top.offset_in_item.0 / size.height.0)                                            .clamp(0.0, 1.0)                                    } else {                                        0.0                                    };                                     PendingScroll::Proportional(PendingScrollFraction {                                        item_ix: scroll_top.item_ix,                                        fraction,                                    })                                })                            })                            .or_else(|| state.pending_scroll.clone())                    }                };            }        }         // Rebuild the tree, replacing items in the range with        // Unmeasured copies that keep their focus handles.        let new_items = {            let mut cursor = state.items.cursor::<Count>(());            let mut new_items = cursor.slice(&Count(range.start), Bias::Right);            let invalidated = cursor.slice(&Count(range.end), Bias::Right);            new_items.extend(                invalidated.iter().map(|item| ListItem::Unmeasured {                    size_hint: item.size_hint(),                    focus_handle: item.focus_handle(),                }),                (),            );            new_items.append(cursor.suffix(), ());            new_items        };        state.items = new_items;        state.measuring_behavior.reset();    }     /// The number of items in this list.    pub fn item_count(&self) -> usize {        self.0.borrow().items.summary().count    }     /// Whether the list is scrolled to the end, or `None` if the list is    /// not scrollable or the total content height is not yet known.    pub fn is_scrolled_to_end(&self) -> Option<bool> {        let state = self.0.borrow();        let bounds = state.last_layout_bounds?;        let summary = state.items.summary();        if summary.has_unknown_height {            return None;        }        let padding = state.last_padding.unwrap_or_default();        let content_height = summary.height + padding.top + padding.bottom;        let scroll_max = (content_height - bounds.size.height).max(px(0.));        if scroll_max <= px(0.) {            return None;        }        let scroll_top = state.scroll_top(&state.logical_scroll_top());        Some(scroll_top >= scroll_max)    } 

Lines 1–500 of 2789.