list.rs

  1use crate::{
  2    point, px, AnyElement, AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
  3    DispatchPhase, Element, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style,
  4    StyleRefinement, Styled, 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    pub fn scroll_to_reveal_item(&self, ix: usize) {
158        let state = &mut *self.0.borrow_mut();
159        let mut scroll_top = state.logical_scroll_top();
160        let height = state
161            .last_layout_bounds
162            .map_or(px(0.), |bounds| bounds.size.height);
163
164        if ix <= scroll_top.item_ix {
165            scroll_top.item_ix = ix;
166            scroll_top.offset_in_item = px(0.);
167        } else {
168            let mut cursor = state.items.cursor::<ListItemSummary>();
169            cursor.seek(&Count(ix + 1), Bias::Right, &());
170            let bottom = cursor.start().height;
171            let goal_top = px(0.).max(bottom - height);
172
173            cursor.seek(&Height(goal_top), Bias::Left, &());
174            let start_ix = cursor.start().count;
175            let start_item_top = cursor.start().height;
176
177            if start_ix >= scroll_top.item_ix {
178                scroll_top.item_ix = start_ix;
179                scroll_top.offset_in_item = goal_top - start_item_top;
180            }
181        }
182
183        state.logical_scroll_top = Some(scroll_top);
184    }
185
186    /// Get the bounds for the given item in window coordinates.
187    pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
188        let state = &*self.0.borrow();
189        let bounds = state.last_layout_bounds.unwrap_or_default();
190        let scroll_top = state.logical_scroll_top();
191
192        if ix < scroll_top.item_ix {
193            return None;
194        }
195
196        let mut cursor = state.items.cursor::<(Count, Height)>();
197        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
198
199        let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item;
200
201        cursor.seek_forward(&Count(ix), Bias::Right, &());
202        if let Some(&ListItem::Rendered { height }) = cursor.item() {
203            let &(Count(count), Height(top)) = cursor.start();
204            if count == ix {
205                let top = bounds.top() + top - scroll_top;
206                return Some(Bounds::from_corners(
207                    point(bounds.left(), top),
208                    point(bounds.right(), top + height),
209                ));
210            }
211        }
212        None
213    }
214}
215
216impl StateInner {
217    fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
218        let mut cursor = self.items.cursor::<ListItemSummary>();
219        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
220        let start_y = cursor.start().height + scroll_top.offset_in_item;
221        cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
222        scroll_top.item_ix..cursor.start().count + 1
223    }
224
225    fn scroll(
226        &mut self,
227        scroll_top: &ListOffset,
228        height: Pixels,
229        delta: Point<Pixels>,
230        cx: &mut WindowContext,
231    ) {
232        let scroll_max = (self.items.summary().height - height).max(px(0.));
233        let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
234            .max(px(0.))
235            .min(scroll_max);
236
237        if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
238            self.logical_scroll_top = None;
239        } else {
240            let mut cursor = self.items.cursor::<ListItemSummary>();
241            cursor.seek(&Height(new_scroll_top), Bias::Right, &());
242            let item_ix = cursor.start().count;
243            let offset_in_item = new_scroll_top - cursor.start().height;
244            self.logical_scroll_top = Some(ListOffset {
245                item_ix,
246                offset_in_item,
247            });
248        }
249
250        if self.scroll_handler.is_some() {
251            let visible_range = self.visible_range(height, scroll_top);
252            self.scroll_handler.as_mut().unwrap()(
253                &ListScrollEvent {
254                    visible_range,
255                    count: self.items.summary().count,
256                },
257                cx,
258            );
259        }
260
261        cx.notify();
262    }
263
264    fn logical_scroll_top(&self) -> ListOffset {
265        self.logical_scroll_top
266            .unwrap_or_else(|| match self.alignment {
267                ListAlignment::Top => ListOffset {
268                    item_ix: 0,
269                    offset_in_item: px(0.),
270                },
271                ListAlignment::Bottom => ListOffset {
272                    item_ix: self.items.summary().count,
273                    offset_in_item: px(0.),
274                },
275            })
276    }
277
278    fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
279        let mut cursor = self.items.cursor::<ListItemSummary>();
280        cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
281        cursor.start().height + logical_scroll_top.offset_in_item
282    }
283}
284
285impl std::fmt::Debug for ListItem {
286    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287        match self {
288            Self::Unrendered => write!(f, "Unrendered"),
289            Self::Rendered { height, .. } => {
290                f.debug_struct("Rendered").field("height", height).finish()
291            }
292        }
293    }
294}
295
296#[derive(Debug, Clone, Copy, Default)]
297pub struct ListOffset {
298    pub item_ix: usize,
299    pub offset_in_item: Pixels,
300}
301
302impl Element for List {
303    type State = ();
304
305    fn request_layout(
306        &mut self,
307        _state: Option<Self::State>,
308        cx: &mut crate::WindowContext,
309    ) -> (crate::LayoutId, Self::State) {
310        let mut style = Style::default();
311        style.refine(&self.style);
312        let layout_id = cx.with_text_style(style.text_style().cloned(), |cx| {
313            cx.request_layout(&style, None)
314        });
315        (layout_id, ())
316    }
317
318    fn paint(
319        &mut self,
320        bounds: Bounds<crate::Pixels>,
321        _state: &mut Self::State,
322        cx: &mut crate::WindowContext,
323    ) {
324        let state = &mut *self.state.0.borrow_mut();
325
326        // If the width of the list has changed, invalidate all cached item heights
327        if state.last_layout_bounds.map_or(true, |last_bounds| {
328            last_bounds.size.width != bounds.size.width
329        }) {
330            state.items = SumTree::from_iter(
331                (0..state.items.summary().count).map(|_| ListItem::Unrendered),
332                &(),
333            )
334        }
335
336        let old_items = state.items.clone();
337        let mut measured_items = VecDeque::new();
338        let mut item_elements = VecDeque::new();
339        let mut rendered_height = px(0.);
340        let mut scroll_top = state.logical_scroll_top();
341
342        let available_item_space = Size {
343            width: AvailableSpace::Definite(bounds.size.width),
344            height: AvailableSpace::MinContent,
345        };
346
347        // Render items after the scroll top, including those in the trailing overdraw
348        let mut cursor = old_items.cursor::<Count>();
349        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
350        for (ix, item) in cursor.by_ref().enumerate() {
351            let visible_height = rendered_height - scroll_top.offset_in_item;
352            if visible_height >= bounds.size.height + state.overdraw {
353                break;
354            }
355
356            // Use the previously cached height if available
357            let mut height = if let ListItem::Rendered { height } = item {
358                Some(*height)
359            } else {
360                None
361            };
362
363            // If we're within the visible area or the height wasn't cached, render and measure the item's element
364            if visible_height < bounds.size.height || height.is_none() {
365                let mut element = (state.render_item)(scroll_top.item_ix + ix, cx);
366                let element_size = element.measure(available_item_space, cx);
367                height = Some(element_size.height);
368                if visible_height < bounds.size.height {
369                    item_elements.push_back(element);
370                }
371            }
372
373            let height = height.unwrap();
374            rendered_height += height;
375            measured_items.push_back(ListItem::Rendered { height });
376        }
377
378        // Prepare to start walking upward from the item at the scroll top.
379        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
380
381        // If the rendered items do not fill the visible region, then adjust
382        // the scroll top upward.
383        if rendered_height - scroll_top.offset_in_item < bounds.size.height {
384            while rendered_height < bounds.size.height {
385                cursor.prev(&());
386                if cursor.item().is_some() {
387                    let mut element = (state.render_item)(cursor.start().0, cx);
388                    let element_size = element.measure(available_item_space, cx);
389
390                    rendered_height += element_size.height;
391                    measured_items.push_front(ListItem::Rendered {
392                        height: element_size.height,
393                    });
394                    item_elements.push_front(element)
395                } else {
396                    break;
397                }
398            }
399
400            scroll_top = ListOffset {
401                item_ix: cursor.start().0,
402                offset_in_item: rendered_height - bounds.size.height,
403            };
404
405            match state.alignment {
406                ListAlignment::Top => {
407                    scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
408                    state.logical_scroll_top = Some(scroll_top);
409                }
410                ListAlignment::Bottom => {
411                    scroll_top = ListOffset {
412                        item_ix: cursor.start().0,
413                        offset_in_item: rendered_height - bounds.size.height,
414                    };
415                    state.logical_scroll_top = None;
416                }
417            };
418        }
419
420        // Measure items in the leading overdraw
421        let mut leading_overdraw = scroll_top.offset_in_item;
422        while leading_overdraw < state.overdraw {
423            cursor.prev(&());
424            if let Some(item) = cursor.item() {
425                let height = if let ListItem::Rendered { height } = item {
426                    *height
427                } else {
428                    let mut element = (state.render_item)(cursor.start().0, cx);
429                    element.measure(available_item_space, cx).height
430                };
431
432                leading_overdraw += height;
433                measured_items.push_front(ListItem::Rendered { height });
434            } else {
435                break;
436            }
437        }
438
439        let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
440        let mut cursor = old_items.cursor::<Count>();
441        let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &());
442        new_items.extend(measured_items, &());
443        cursor.seek(&Count(measured_range.end), Bias::Right, &());
444        new_items.append(cursor.suffix(&()), &());
445
446        // Paint the visible items
447        cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
448            let mut item_origin = bounds.origin;
449            item_origin.y -= scroll_top.offset_in_item;
450            for item_element in &mut item_elements {
451                let item_height = item_element.measure(available_item_space, cx).height;
452                item_element.draw(item_origin, available_item_space, cx);
453                item_origin.y += item_height;
454            }
455        });
456
457        state.items = new_items;
458        state.last_layout_bounds = Some(bounds);
459
460        let list_state = self.state.clone();
461        let height = bounds.size.height;
462        cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
463            if phase == DispatchPhase::Bubble
464                && bounds.contains(&event.position)
465                && cx.was_top_layer(&event.position, cx.stacking_order())
466            {
467                list_state.0.borrow_mut().scroll(
468                    &scroll_top,
469                    height,
470                    event.delta.pixel_delta(px(20.)),
471                    cx,
472                )
473            }
474        });
475    }
476}
477
478impl IntoElement for List {
479    type Element = Self;
480
481    fn element_id(&self) -> Option<crate::ElementId> {
482        None
483    }
484
485    fn into_element(self) -> Self::Element {
486        self
487    }
488}
489
490impl Styled for List {
491    fn style(&mut self) -> &mut StyleRefinement {
492        &mut self.style
493    }
494}
495
496impl sum_tree::Item for ListItem {
497    type Summary = ListItemSummary;
498
499    fn summary(&self) -> Self::Summary {
500        match self {
501            ListItem::Unrendered => ListItemSummary {
502                count: 1,
503                rendered_count: 0,
504                unrendered_count: 1,
505                height: px(0.),
506            },
507            ListItem::Rendered { height } => ListItemSummary {
508                count: 1,
509                rendered_count: 1,
510                unrendered_count: 0,
511                height: *height,
512            },
513        }
514    }
515}
516
517impl sum_tree::Summary for ListItemSummary {
518    type Context = ();
519
520    fn add_summary(&mut self, summary: &Self, _: &()) {
521        self.count += summary.count;
522        self.rendered_count += summary.rendered_count;
523        self.unrendered_count += summary.unrendered_count;
524        self.height += summary.height;
525    }
526}
527
528impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
529    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
530        self.0 += summary.count;
531    }
532}
533
534impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
535    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
536        self.0 += summary.rendered_count;
537    }
538}
539
540impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
541    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
542        self.0 += summary.unrendered_count;
543    }
544}
545
546impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
547    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
548        self.0 += summary.height;
549    }
550}
551
552impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
553    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
554        self.0.partial_cmp(&other.count).unwrap()
555    }
556}
557
558impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
559    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
560        self.0.partial_cmp(&other.height).unwrap()
561    }
562}