list.rs

  1use crate::{
  2    point, px, AnyElement, AvailableSpace, BorrowAppContext, Bounds, DispatchPhase, Element,
  3    IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
  4    WindowContext,
  5};
  6use collections::VecDeque;
  7use refineable::Refineable as _;
  8use std::{cell::RefCell, ops::Range, rc::Rc};
  9use sum_tree::{Bias, SumTree};
 10
 11pub fn list(state: ListState) -> List {
 12    List {
 13        state,
 14        style: StyleRefinement::default(),
 15    }
 16}
 17
 18pub struct List {
 19    state: ListState,
 20    style: StyleRefinement,
 21}
 22
 23#[derive(Clone)]
 24pub struct ListState(Rc<RefCell<StateInner>>);
 25
 26struct StateInner {
 27    last_layout_bounds: Option<Bounds<Pixels>>,
 28    render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
 29    items: SumTree<ListItem>,
 30    logical_scroll_top: Option<ListOffset>,
 31    alignment: ListAlignment,
 32    overdraw: Pixels,
 33    #[allow(clippy::type_complexity)]
 34    scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
 35}
 36
 37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
 38pub enum ListAlignment {
 39    Top,
 40    Bottom,
 41}
 42
 43pub struct ListScrollEvent {
 44    pub visible_range: Range<usize>,
 45    pub count: usize,
 46}
 47
 48#[derive(Clone)]
 49enum ListItem {
 50    Unrendered,
 51    Rendered { height: Pixels },
 52}
 53
 54#[derive(Clone, Debug, Default, PartialEq)]
 55struct ListItemSummary {
 56    count: usize,
 57    rendered_count: usize,
 58    unrendered_count: usize,
 59    height: Pixels,
 60}
 61
 62#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
 63struct Count(usize);
 64
 65#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
 66struct RenderedCount(usize);
 67
 68#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
 69struct UnrenderedCount(usize);
 70
 71#[derive(Clone, Debug, Default)]
 72struct Height(Pixels);
 73
 74impl ListState {
 75    pub fn new<F>(
 76        element_count: usize,
 77        orientation: ListAlignment,
 78        overdraw: Pixels,
 79        render_item: F,
 80    ) -> Self
 81    where
 82        F: 'static + FnMut(usize, &mut WindowContext) -> AnyElement,
 83    {
 84        let mut items = SumTree::new();
 85        items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
 86        Self(Rc::new(RefCell::new(StateInner {
 87            last_layout_bounds: None,
 88            render_item: Box::new(render_item),
 89            items,
 90            logical_scroll_top: None,
 91            alignment: orientation,
 92            overdraw,
 93            scroll_handler: None,
 94        })))
 95    }
 96
 97    pub fn reset(&self, element_count: usize) {
 98        let state = &mut *self.0.borrow_mut();
 99        state.logical_scroll_top = None;
100        state.items = SumTree::new();
101        state
102            .items
103            .extend((0..element_count).map(|_| ListItem::Unrendered), &());
104    }
105
106    pub fn item_count(&self) -> usize {
107        self.0.borrow().items.summary().count
108    }
109
110    pub fn splice(&self, old_range: Range<usize>, count: usize) {
111        let state = &mut *self.0.borrow_mut();
112
113        if let Some(ListOffset {
114            item_ix,
115            offset_in_item,
116        }) = state.logical_scroll_top.as_mut()
117        {
118            if old_range.contains(item_ix) {
119                *item_ix = old_range.start;
120                *offset_in_item = px(0.);
121            } else if old_range.end <= *item_ix {
122                *item_ix = *item_ix - (old_range.end - old_range.start) + count;
123            }
124        }
125
126        let mut old_heights = state.items.cursor::<Count>();
127        let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
128        old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
129
130        new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
131        new_heights.append(old_heights.suffix(&()), &());
132        drop(old_heights);
133        state.items = new_heights;
134    }
135
136    pub fn set_scroll_handler(
137        &self,
138        handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
139    ) {
140        self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
141    }
142
143    pub fn logical_scroll_top(&self) -> ListOffset {
144        self.0.borrow().logical_scroll_top()
145    }
146
147    pub fn scroll_to(&self, mut scroll_top: ListOffset) {
148        let state = &mut *self.0.borrow_mut();
149        let item_count = state.items.summary().count;
150        if scroll_top.item_ix >= item_count {
151            scroll_top.item_ix = item_count;
152            scroll_top.offset_in_item = px(0.);
153        }
154        state.logical_scroll_top = Some(scroll_top);
155    }
156
157    /// Get the bounds for the given item in window coordinates.
158    pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
159        let state = &*self.0.borrow();
160        let bounds = state.last_layout_bounds.unwrap_or_default();
161        let scroll_top = state.logical_scroll_top.unwrap_or_default();
162
163        if ix < scroll_top.item_ix {
164            return None;
165        }
166
167        let mut cursor = state.items.cursor::<(Count, Height)>();
168        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
169
170        let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item;
171
172        cursor.seek_forward(&Count(ix), Bias::Right, &());
173        if let Some(&ListItem::Rendered { height }) = cursor.item() {
174            let &(Count(count), Height(top)) = cursor.start();
175            if count == ix {
176                let top = bounds.top() + top - scroll_top;
177                return Some(Bounds::from_corners(
178                    point(bounds.left(), top),
179                    point(bounds.right(), top + height),
180                ));
181            }
182        }
183        None
184    }
185}
186
187impl StateInner {
188    fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
189        let mut cursor = self.items.cursor::<ListItemSummary>();
190        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
191        let start_y = cursor.start().height + scroll_top.offset_in_item;
192        cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
193        scroll_top.item_ix..cursor.start().count + 1
194    }
195
196    fn scroll(
197        &mut self,
198        scroll_top: &ListOffset,
199        height: Pixels,
200        delta: Point<Pixels>,
201        cx: &mut WindowContext,
202    ) {
203        let scroll_max = (self.items.summary().height - height).max(px(0.));
204        let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
205            .max(px(0.))
206            .min(scroll_max);
207
208        if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
209            self.logical_scroll_top = None;
210        } else {
211            let mut cursor = self.items.cursor::<ListItemSummary>();
212            cursor.seek(&Height(new_scroll_top), Bias::Right, &());
213            let item_ix = cursor.start().count;
214            let offset_in_item = new_scroll_top - cursor.start().height;
215            self.logical_scroll_top = Some(ListOffset {
216                item_ix,
217                offset_in_item,
218            });
219        }
220
221        if self.scroll_handler.is_some() {
222            let visible_range = self.visible_range(height, scroll_top);
223            self.scroll_handler.as_mut().unwrap()(
224                &ListScrollEvent {
225                    visible_range,
226                    count: self.items.summary().count,
227                },
228                cx,
229            );
230        }
231
232        cx.notify();
233    }
234
235    fn logical_scroll_top(&self) -> ListOffset {
236        self.logical_scroll_top
237            .unwrap_or_else(|| match self.alignment {
238                ListAlignment::Top => ListOffset {
239                    item_ix: 0,
240                    offset_in_item: px(0.),
241                },
242                ListAlignment::Bottom => ListOffset {
243                    item_ix: self.items.summary().count,
244                    offset_in_item: px(0.),
245                },
246            })
247    }
248
249    fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
250        let mut cursor = self.items.cursor::<ListItemSummary>();
251        cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
252        cursor.start().height + logical_scroll_top.offset_in_item
253    }
254}
255
256impl std::fmt::Debug for ListItem {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        match self {
259            Self::Unrendered => write!(f, "Unrendered"),
260            Self::Rendered { height, .. } => {
261                f.debug_struct("Rendered").field("height", height).finish()
262            }
263        }
264    }
265}
266
267#[derive(Debug, Clone, Copy, Default)]
268pub struct ListOffset {
269    pub item_ix: usize,
270    pub offset_in_item: Pixels,
271}
272
273impl Element for List {
274    type State = ();
275
276    fn layout(
277        &mut self,
278        _state: Option<Self::State>,
279        cx: &mut crate::WindowContext,
280    ) -> (crate::LayoutId, Self::State) {
281        let mut style = Style::default();
282        style.refine(&self.style);
283        let layout_id = cx.with_text_style(style.text_style().cloned(), |cx| {
284            cx.request_layout(&style, None)
285        });
286        (layout_id, ())
287    }
288
289    fn paint(
290        &mut self,
291        bounds: crate::Bounds<crate::Pixels>,
292        _state: &mut Self::State,
293        cx: &mut crate::WindowContext,
294    ) {
295        let state = &mut *self.state.0.borrow_mut();
296
297        // If the width of the list has changed, invalidate all cached item heights
298        if state.last_layout_bounds.map_or(true, |last_bounds| {
299            last_bounds.size.width != bounds.size.width
300        }) {
301            state.items = SumTree::from_iter(
302                (0..state.items.summary().count).map(|_| ListItem::Unrendered),
303                &(),
304            )
305        }
306
307        let old_items = state.items.clone();
308        let mut measured_items = VecDeque::new();
309        let mut item_elements = VecDeque::new();
310        let mut rendered_height = px(0.);
311        let mut scroll_top = state.logical_scroll_top();
312
313        let available_item_space = Size {
314            width: AvailableSpace::Definite(bounds.size.width),
315            height: AvailableSpace::MinContent,
316        };
317
318        // Render items after the scroll top, including those in the trailing overdraw
319        let mut cursor = old_items.cursor::<Count>();
320        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
321        for (ix, item) in cursor.by_ref().enumerate() {
322            let visible_height = rendered_height - scroll_top.offset_in_item;
323            if visible_height >= bounds.size.height + state.overdraw {
324                break;
325            }
326
327            // Use the previously cached height if available
328            let mut height = if let ListItem::Rendered { height } = item {
329                Some(*height)
330            } else {
331                None
332            };
333
334            // If we're within the visible area or the height wasn't cached, render and measure the item's element
335            if visible_height < bounds.size.height || height.is_none() {
336                let mut element = (state.render_item)(scroll_top.item_ix + ix, cx);
337                let element_size = element.measure(available_item_space, cx);
338                height = Some(element_size.height);
339                if visible_height < bounds.size.height {
340                    item_elements.push_back(element);
341                }
342            }
343
344            let height = height.unwrap();
345            rendered_height += height;
346            measured_items.push_back(ListItem::Rendered { height });
347        }
348
349        // Prepare to start walking upward from the item at the scroll top.
350        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
351
352        // If the rendered items do not fill the visible region, then adjust
353        // the scroll top upward.
354        if rendered_height - scroll_top.offset_in_item < bounds.size.height {
355            while rendered_height < bounds.size.height {
356                cursor.prev(&());
357                if cursor.item().is_some() {
358                    let mut element = (state.render_item)(cursor.start().0, cx);
359                    let element_size = element.measure(available_item_space, cx);
360
361                    rendered_height += element_size.height;
362                    measured_items.push_front(ListItem::Rendered {
363                        height: element_size.height,
364                    });
365                    item_elements.push_front(element)
366                } else {
367                    break;
368                }
369            }
370
371            scroll_top = ListOffset {
372                item_ix: cursor.start().0,
373                offset_in_item: rendered_height - bounds.size.height,
374            };
375
376            match state.alignment {
377                ListAlignment::Top => {
378                    scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
379                    state.logical_scroll_top = Some(scroll_top);
380                }
381                ListAlignment::Bottom => {
382                    scroll_top = ListOffset {
383                        item_ix: cursor.start().0,
384                        offset_in_item: rendered_height - bounds.size.height,
385                    };
386                    state.logical_scroll_top = None;
387                }
388            };
389        }
390
391        // Measure items in the leading overdraw
392        let mut leading_overdraw = scroll_top.offset_in_item;
393        while leading_overdraw < state.overdraw {
394            cursor.prev(&());
395            if let Some(item) = cursor.item() {
396                let height = if let ListItem::Rendered { height } = item {
397                    *height
398                } else {
399                    let mut element = (state.render_item)(cursor.start().0, cx);
400                    element.measure(available_item_space, cx).height
401                };
402
403                leading_overdraw += height;
404                measured_items.push_front(ListItem::Rendered { height });
405            } else {
406                break;
407            }
408        }
409
410        let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
411        let mut cursor = old_items.cursor::<Count>();
412        let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &());
413        new_items.extend(measured_items, &());
414        cursor.seek(&Count(measured_range.end), Bias::Right, &());
415        new_items.append(cursor.suffix(&()), &());
416
417        // Paint the visible items
418        let mut item_origin = bounds.origin;
419        item_origin.y -= scroll_top.offset_in_item;
420        for item_element in &mut item_elements {
421            let item_height = item_element.measure(available_item_space, cx).height;
422            item_element.draw(item_origin, available_item_space, cx);
423            item_origin.y += item_height;
424        }
425
426        state.items = new_items;
427        state.last_layout_bounds = Some(bounds);
428
429        let list_state = self.state.clone();
430        let height = bounds.size.height;
431        cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
432            if phase == DispatchPhase::Bubble {
433                list_state.0.borrow_mut().scroll(
434                    &scroll_top,
435                    height,
436                    event.delta.pixel_delta(px(20.)),
437                    cx,
438                )
439            }
440        });
441    }
442}
443
444impl IntoElement for List {
445    type Element = Self;
446
447    fn element_id(&self) -> Option<crate::ElementId> {
448        None
449    }
450
451    fn into_element(self) -> Self::Element {
452        self
453    }
454}
455
456impl Styled for List {
457    fn style(&mut self) -> &mut StyleRefinement {
458        &mut self.style
459    }
460}
461
462impl sum_tree::Item for ListItem {
463    type Summary = ListItemSummary;
464
465    fn summary(&self) -> Self::Summary {
466        match self {
467            ListItem::Unrendered => ListItemSummary {
468                count: 1,
469                rendered_count: 0,
470                unrendered_count: 1,
471                height: px(0.),
472            },
473            ListItem::Rendered { height } => ListItemSummary {
474                count: 1,
475                rendered_count: 1,
476                unrendered_count: 0,
477                height: *height,
478            },
479        }
480    }
481}
482
483impl sum_tree::Summary for ListItemSummary {
484    type Context = ();
485
486    fn add_summary(&mut self, summary: &Self, _: &()) {
487        self.count += summary.count;
488        self.rendered_count += summary.rendered_count;
489        self.unrendered_count += summary.unrendered_count;
490        self.height += summary.height;
491    }
492}
493
494impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
495    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
496        self.0 += summary.count;
497    }
498}
499
500impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
501    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
502        self.0 += summary.rendered_count;
503    }
504}
505
506impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
507    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
508        self.0 += summary.unrendered_count;
509    }
510}
511
512impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
513    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
514        self.0 += summary.height;
515    }
516}
517
518impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
519    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
520        self.0.partial_cmp(&other.count).unwrap()
521    }
522}
523
524impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
525    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
526        self.0.partial_cmp(&other.height).unwrap()
527    }
528}