list.rs

  1use crate::{
  2    geometry::{
  3        rect::RectF,
  4        vector::{vec2f, Vector2F},
  5    },
  6    json::json,
  7    AnyElement, Element, MouseRegion, SizeConstraint, ViewContext,
  8};
  9use std::{cell::RefCell, collections::VecDeque, fmt::Debug, ops::Range, rc::Rc};
 10use sum_tree::{Bias, SumTree};
 11
 12pub struct List<V> {
 13    state: ListState<V>,
 14}
 15
 16pub struct ListState<V>(Rc<RefCell<StateInner<V>>>);
 17
 18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
 19pub enum Orientation {
 20    Top,
 21    Bottom,
 22}
 23
 24struct StateInner<V> {
 25    last_layout_width: Option<f32>,
 26    render_item: Box<dyn FnMut(&mut V, usize, &mut ViewContext<V>) -> AnyElement<V>>,
 27    rendered_range: Range<usize>,
 28    items: SumTree<ListItem<V>>,
 29    logical_scroll_top: Option<ListOffset>,
 30    orientation: Orientation,
 31    overdraw: f32,
 32    #[allow(clippy::type_complexity)]
 33    scroll_handler: Option<Box<dyn FnMut(Range<usize>, usize, &mut V, &mut ViewContext<V>)>>,
 34}
 35
 36#[derive(Clone, Copy, Debug, Default, PartialEq)]
 37pub struct ListOffset {
 38    pub item_ix: usize,
 39    pub offset_in_item: f32,
 40}
 41
 42enum ListItem<V> {
 43    Unrendered,
 44    Rendered(Rc<RefCell<AnyElement<V>>>),
 45    Removed(f32),
 46}
 47
 48impl<V> Clone for ListItem<V> {
 49    fn clone(&self) -> Self {
 50        match self {
 51            Self::Unrendered => Self::Unrendered,
 52            Self::Rendered(element) => Self::Rendered(element.clone()),
 53            Self::Removed(height) => Self::Removed(*height),
 54        }
 55    }
 56}
 57
 58impl<V> Debug for ListItem<V> {
 59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 60        match self {
 61            Self::Unrendered => write!(f, "Unrendered"),
 62            Self::Rendered(_) => f.debug_tuple("Rendered").finish(),
 63            Self::Removed(height) => f.debug_tuple("Removed").field(height).finish(),
 64        }
 65    }
 66}
 67
 68#[derive(Clone, Debug, Default, PartialEq)]
 69struct ListItemSummary {
 70    count: usize,
 71    rendered_count: usize,
 72    unrendered_count: usize,
 73    height: f32,
 74}
 75
 76#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
 77struct Count(usize);
 78
 79#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
 80struct RenderedCount(usize);
 81
 82#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
 83struct UnrenderedCount(usize);
 84
 85#[derive(Clone, Debug, Default)]
 86struct Height(f32);
 87
 88impl<V> List<V> {
 89    pub fn new(state: ListState<V>) -> Self {
 90        Self { state }
 91    }
 92}
 93
 94impl<V: 'static> Element<V> for List<V> {
 95    type LayoutState = ListOffset;
 96    type PaintState = ();
 97
 98    fn layout(
 99        &mut self,
100        constraint: SizeConstraint,
101        view: &mut V,
102        cx: &mut ViewContext<V>,
103    ) -> (Vector2F, Self::LayoutState) {
104        let state = &mut *self.state.0.borrow_mut();
105        let size = constraint.max;
106        let mut item_constraint = constraint;
107        item_constraint.min.set_y(0.);
108        item_constraint.max.set_y(f32::INFINITY);
109
110        if cx.refreshing() || state.last_layout_width != Some(size.x()) {
111            state.rendered_range = 0..0;
112            state.items = SumTree::from_iter(
113                (0..state.items.summary().count).map(|_| ListItem::Unrendered),
114                &(),
115            )
116        }
117
118        let old_items = state.items.clone();
119        let mut new_items = SumTree::new();
120        let mut rendered_items = VecDeque::new();
121        let mut rendered_height = 0.;
122        let mut scroll_top = state.logical_scroll_top();
123
124        // Render items after the scroll top, including those in the trailing overdraw.
125        let mut cursor = old_items.cursor::<Count>();
126        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
127        for (ix, item) in cursor.by_ref().enumerate() {
128            let visible_height = rendered_height - scroll_top.offset_in_item;
129            if visible_height >= size.y() + state.overdraw {
130                break;
131            }
132
133            // Force re-render if the item is visible, but attempt to re-use an existing one
134            // if we are inside the overdraw.
135            let existing_element = if visible_height >= size.y() {
136                Some(item)
137            } else {
138                None
139            };
140            if let Some(element) = state.render_item(
141                scroll_top.item_ix + ix,
142                existing_element,
143                item_constraint,
144                view,
145                cx,
146            ) {
147                rendered_height += element.borrow().size().y();
148                rendered_items.push_back(ListItem::Rendered(element));
149            }
150        }
151
152        // Prepare to start walking upward from the item at the scroll top.
153        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
154
155        // If the rendered items do not fill the visible region, then adjust
156        // the scroll top upward.
157        if rendered_height - scroll_top.offset_in_item < size.y() {
158            while rendered_height < size.y() {
159                cursor.prev(&());
160                if cursor.item().is_some() {
161                    if let Some(element) =
162                        state.render_item(cursor.start().0, None, item_constraint, view, cx)
163                    {
164                        rendered_height += element.borrow().size().y();
165                        rendered_items.push_front(ListItem::Rendered(element));
166                    }
167                } else {
168                    break;
169                }
170            }
171
172            scroll_top = ListOffset {
173                item_ix: cursor.start().0,
174                offset_in_item: rendered_height - size.y(),
175            };
176
177            match state.orientation {
178                Orientation::Top => {
179                    scroll_top.offset_in_item = scroll_top.offset_in_item.max(0.);
180                    state.logical_scroll_top = Some(scroll_top);
181                }
182                Orientation::Bottom => {
183                    scroll_top = ListOffset {
184                        item_ix: cursor.start().0,
185                        offset_in_item: rendered_height - size.y(),
186                    };
187                    state.logical_scroll_top = None;
188                }
189            };
190        }
191
192        // Render items in the leading overdraw.
193        let mut leading_overdraw = scroll_top.offset_in_item;
194        while leading_overdraw < state.overdraw {
195            cursor.prev(&());
196            if let Some(item) = cursor.item() {
197                if let Some(element) =
198                    state.render_item(cursor.start().0, Some(item), item_constraint, view, cx)
199                {
200                    leading_overdraw += element.borrow().size().y();
201                    rendered_items.push_front(ListItem::Rendered(element));
202                }
203            } else {
204                break;
205            }
206        }
207
208        let new_rendered_range = cursor.start().0..(cursor.start().0 + rendered_items.len());
209
210        let mut cursor = old_items.cursor::<Count>();
211
212        if state.rendered_range.start < new_rendered_range.start {
213            new_items.append(
214                cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
215                &(),
216            );
217            let remove_to = state.rendered_range.end.min(new_rendered_range.start);
218            while cursor.start().0 < remove_to {
219                new_items.push(cursor.item().unwrap().remove(), &());
220                cursor.next(&());
221            }
222        }
223        new_items.append(
224            cursor.slice(&Count(new_rendered_range.start), Bias::Right, &()),
225            &(),
226        );
227
228        new_items.extend(rendered_items, &());
229        cursor.seek(&Count(new_rendered_range.end), Bias::Right, &());
230
231        if new_rendered_range.end < state.rendered_range.start {
232            new_items.append(
233                cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
234                &(),
235            );
236        }
237        while cursor.start().0 < state.rendered_range.end {
238            new_items.push(cursor.item().unwrap().remove(), &());
239            cursor.next(&());
240        }
241
242        new_items.append(cursor.suffix(&()), &());
243
244        state.items = new_items;
245        state.rendered_range = new_rendered_range;
246        state.last_layout_width = Some(size.x());
247        (size, scroll_top)
248    }
249
250    fn paint(
251        &mut self,
252        bounds: RectF,
253        visible_bounds: RectF,
254        scroll_top: &mut ListOffset,
255        view: &mut V,
256        cx: &mut ViewContext<V>,
257    ) {
258        let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
259        cx.scene().push_layer(Some(visible_bounds));
260        let view_id = cx.view_id();
261        cx.scene()
262            .push_mouse_region(MouseRegion::new::<Self>(view_id, 0, bounds).on_scroll({
263                let state = self.state.clone();
264                let height = bounds.height();
265                let scroll_top = scroll_top.clone();
266                move |e, view, cx| {
267                    state.0.borrow_mut().scroll(
268                        &scroll_top,
269                        height,
270                        *e.platform_event.delta.raw(),
271                        e.platform_event.delta.precise(),
272                        view,
273                        cx,
274                    )
275                }
276            }));
277
278        let state = &mut *self.state.0.borrow_mut();
279        for (element, origin) in state.visible_elements(bounds, scroll_top) {
280            element.borrow_mut().paint(origin, visible_bounds, view, cx);
281        }
282
283        cx.scene().pop_layer();
284    }
285
286    fn rect_for_text_range(
287        &self,
288        range_utf16: Range<usize>,
289        bounds: RectF,
290        _: RectF,
291        scroll_top: &Self::LayoutState,
292        _: &Self::PaintState,
293        view: &V,
294        cx: &ViewContext<V>,
295    ) -> Option<RectF> {
296        let state = self.state.0.borrow();
297        let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
298        let mut cursor = state.items.cursor::<Count>();
299        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
300        while let Some(item) = cursor.item() {
301            if item_origin.y() > bounds.max_y() {
302                break;
303            }
304
305            if let ListItem::Rendered(element) = item {
306                if let Some(rect) =
307                    element
308                        .borrow()
309                        .rect_for_text_range(range_utf16.clone(), view, cx)
310                {
311                    return Some(rect);
312                }
313
314                item_origin.set_y(item_origin.y() + element.borrow().size().y());
315                cursor.next(&());
316            } else {
317                unreachable!();
318            }
319        }
320
321        None
322    }
323
324    fn debug(
325        &self,
326        bounds: RectF,
327        scroll_top: &Self::LayoutState,
328        _: &(),
329        view: &V,
330        cx: &ViewContext<V>,
331    ) -> serde_json::Value {
332        let state = self.state.0.borrow_mut();
333        let visible_elements = state
334            .visible_elements(bounds, scroll_top)
335            .map(|e| e.0.borrow().debug(view, cx))
336            .collect::<Vec<_>>();
337        let visible_range = scroll_top.item_ix..(scroll_top.item_ix + visible_elements.len());
338        json!({
339            "visible_range": visible_range,
340            "visible_elements": visible_elements,
341            "scroll_top": state.logical_scroll_top.map(|top| (top.item_ix, top.offset_in_item)),
342        })
343    }
344}
345
346impl<V: 'static> ListState<V> {
347    pub fn new<D, F>(
348        element_count: usize,
349        orientation: Orientation,
350        overdraw: f32,
351        mut render_item: F,
352    ) -> Self
353    where
354        D: Element<V>,
355        F: 'static + FnMut(&mut V, usize, &mut ViewContext<V>) -> D,
356    {
357        let mut items = SumTree::new();
358        items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
359        Self(Rc::new(RefCell::new(StateInner {
360            last_layout_width: None,
361            render_item: Box::new(move |view, ix, cx| render_item(view, ix, cx).into_any()),
362            rendered_range: 0..0,
363            items,
364            logical_scroll_top: None,
365            orientation,
366            overdraw,
367            scroll_handler: None,
368        })))
369    }
370
371    pub fn reset(&self, element_count: usize) {
372        let state = &mut *self.0.borrow_mut();
373        state.rendered_range = 0..0;
374        state.logical_scroll_top = None;
375        state.items = SumTree::new();
376        state
377            .items
378            .extend((0..element_count).map(|_| ListItem::Unrendered), &());
379    }
380
381    pub fn item_count(&self) -> usize {
382        self.0.borrow().items.summary().count
383    }
384
385    pub fn splice(&self, old_range: Range<usize>, count: usize) {
386        let state = &mut *self.0.borrow_mut();
387
388        if let Some(ListOffset {
389            item_ix,
390            offset_in_item,
391        }) = state.logical_scroll_top.as_mut()
392        {
393            if old_range.contains(item_ix) {
394                *item_ix = old_range.start;
395                *offset_in_item = 0.;
396            } else if old_range.end <= *item_ix {
397                *item_ix = *item_ix - (old_range.end - old_range.start) + count;
398            }
399        }
400
401        let new_end = old_range.start + count;
402        if old_range.start < state.rendered_range.start {
403            state.rendered_range.start =
404                new_end + state.rendered_range.start.saturating_sub(old_range.end);
405        }
406        if old_range.start < state.rendered_range.end {
407            state.rendered_range.end =
408                new_end + state.rendered_range.end.saturating_sub(old_range.end);
409        }
410
411        let mut old_heights = state.items.cursor::<Count>();
412        let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
413        old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
414
415        new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
416        new_heights.append(old_heights.suffix(&()), &());
417        drop(old_heights);
418        state.items = new_heights;
419    }
420
421    pub fn set_scroll_handler(
422        &mut self,
423        handler: impl FnMut(Range<usize>, usize, &mut V, &mut ViewContext<V>) + 'static,
424    ) {
425        self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
426    }
427
428    pub fn logical_scroll_top(&self) -> ListOffset {
429        self.0.borrow().logical_scroll_top()
430    }
431
432    pub fn scroll_to(&self, mut scroll_top: ListOffset) {
433        let state = &mut *self.0.borrow_mut();
434        let item_count = state.items.summary().count;
435        if scroll_top.item_ix >= item_count {
436            scroll_top.item_ix = item_count;
437            scroll_top.offset_in_item = 0.;
438        }
439        state.logical_scroll_top = Some(scroll_top);
440    }
441}
442
443impl<V> Clone for ListState<V> {
444    fn clone(&self) -> Self {
445        Self(self.0.clone())
446    }
447}
448
449impl<V: 'static> StateInner<V> {
450    fn render_item(
451        &mut self,
452        ix: usize,
453        existing_element: Option<&ListItem<V>>,
454        constraint: SizeConstraint,
455        view: &mut V,
456        cx: &mut ViewContext<V>,
457    ) -> Option<Rc<RefCell<AnyElement<V>>>> {
458        if let Some(ListItem::Rendered(element)) = existing_element {
459            Some(element.clone())
460        } else {
461            let mut element = (self.render_item)(view, ix, cx);
462            element.layout(constraint, view, cx);
463            Some(Rc::new(RefCell::new(element)))
464        }
465    }
466
467    fn visible_range(&self, height: f32, scroll_top: &ListOffset) -> Range<usize> {
468        let mut cursor = self.items.cursor::<ListItemSummary>();
469        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
470        let start_y = cursor.start().height + scroll_top.offset_in_item;
471        cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
472        scroll_top.item_ix..cursor.start().count + 1
473    }
474
475    fn visible_elements<'a>(
476        &'a self,
477        bounds: RectF,
478        scroll_top: &ListOffset,
479    ) -> impl Iterator<Item = (Rc<RefCell<AnyElement<V>>>, Vector2F)> + 'a {
480        let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
481        let mut cursor = self.items.cursor::<Count>();
482        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
483        std::iter::from_fn(move || {
484            while let Some(item) = cursor.item() {
485                if item_origin.y() > bounds.max_y() {
486                    break;
487                }
488
489                if let ListItem::Rendered(element) = item {
490                    let result = (element.clone(), item_origin);
491                    item_origin.set_y(item_origin.y() + element.borrow().size().y());
492                    cursor.next(&());
493                    return Some(result);
494                }
495
496                cursor.next(&());
497            }
498
499            None
500        })
501    }
502
503    fn scroll(
504        &mut self,
505        scroll_top: &ListOffset,
506        height: f32,
507        mut delta: Vector2F,
508        precise: bool,
509        view: &mut V,
510        cx: &mut ViewContext<V>,
511    ) {
512        if !precise {
513            delta *= 20.;
514        }
515
516        let scroll_max = (self.items.summary().height - height).max(0.);
517        let new_scroll_top = (self.scroll_top(scroll_top) - delta.y())
518            .max(0.)
519            .min(scroll_max);
520
521        if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max {
522            self.logical_scroll_top = None;
523        } else {
524            let mut cursor = self.items.cursor::<ListItemSummary>();
525            cursor.seek(&Height(new_scroll_top), Bias::Right, &());
526            let item_ix = cursor.start().count;
527            let offset_in_item = new_scroll_top - cursor.start().height;
528            self.logical_scroll_top = Some(ListOffset {
529                item_ix,
530                offset_in_item,
531            });
532        }
533
534        if self.scroll_handler.is_some() {
535            let visible_range = self.visible_range(height, scroll_top);
536            self.scroll_handler.as_mut().unwrap()(
537                visible_range,
538                self.items.summary().count,
539                view,
540                cx,
541            );
542        }
543
544        cx.notify();
545    }
546
547    fn logical_scroll_top(&self) -> ListOffset {
548        self.logical_scroll_top
549            .unwrap_or_else(|| match self.orientation {
550                Orientation::Top => ListOffset {
551                    item_ix: 0,
552                    offset_in_item: 0.,
553                },
554                Orientation::Bottom => ListOffset {
555                    item_ix: self.items.summary().count,
556                    offset_in_item: 0.,
557                },
558            })
559    }
560
561    fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
562        let mut cursor = self.items.cursor::<ListItemSummary>();
563        cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
564        cursor.start().height + logical_scroll_top.offset_in_item
565    }
566}
567
568impl<V> ListItem<V> {
569    fn remove(&self) -> Self {
570        match self {
571            ListItem::Unrendered => ListItem::Unrendered,
572            ListItem::Rendered(element) => ListItem::Removed(element.borrow().size().y()),
573            ListItem::Removed(height) => ListItem::Removed(*height),
574        }
575    }
576}
577
578impl<V> sum_tree::Item for ListItem<V> {
579    type Summary = ListItemSummary;
580
581    fn summary(&self) -> Self::Summary {
582        match self {
583            ListItem::Unrendered => ListItemSummary {
584                count: 1,
585                rendered_count: 0,
586                unrendered_count: 1,
587                height: 0.,
588            },
589            ListItem::Rendered(element) => ListItemSummary {
590                count: 1,
591                rendered_count: 1,
592                unrendered_count: 0,
593                height: element.borrow().size().y(),
594            },
595            ListItem::Removed(height) => ListItemSummary {
596                count: 1,
597                rendered_count: 0,
598                unrendered_count: 1,
599                height: *height,
600            },
601        }
602    }
603}
604
605impl sum_tree::Summary for ListItemSummary {
606    type Context = ();
607
608    fn add_summary(&mut self, summary: &Self, _: &()) {
609        self.count += summary.count;
610        self.rendered_count += summary.rendered_count;
611        self.unrendered_count += summary.unrendered_count;
612        self.height += summary.height;
613    }
614}
615
616impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
617    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
618        self.0 += summary.count;
619    }
620}
621
622impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
623    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
624        self.0 += summary.rendered_count;
625    }
626}
627
628impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
629    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
630        self.0 += summary.unrendered_count;
631    }
632}
633
634impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
635    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
636        self.0 += summary.height;
637    }
638}
639
640impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
641    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
642        self.0.partial_cmp(&other.count).unwrap()
643    }
644}
645
646impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
647    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
648        self.0.partial_cmp(&other.height).unwrap()
649    }
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655    use crate::{elements::Empty, geometry::vector::vec2f, Entity};
656    use rand::prelude::*;
657    use std::env;
658
659    #[crate::test(self)]
660    fn test_layout(cx: &mut crate::AppContext) {
661        cx.add_window(Default::default(), |cx| {
662            let mut view = TestView;
663            let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
664            let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)]));
665            let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, {
666                let elements = elements.clone();
667                move |_, ix, _| {
668                    let (id, height) = elements.borrow()[ix];
669                    TestElement::new(id, height).into_any()
670                }
671            });
672
673            let mut list = List::new(state.clone());
674            let (size, _) = list.layout(constraint, &mut view, cx);
675            assert_eq!(size, vec2f(100., 40.));
676            assert_eq!(
677                state.0.borrow().items.summary().clone(),
678                ListItemSummary {
679                    count: 3,
680                    rendered_count: 3,
681                    unrendered_count: 0,
682                    height: 150.
683                }
684            );
685
686            state.0.borrow_mut().scroll(
687                &ListOffset {
688                    item_ix: 0,
689                    offset_in_item: 0.,
690                },
691                40.,
692                vec2f(0., -54.),
693                true,
694                &mut view,
695                cx,
696            );
697
698            let (_, logical_scroll_top) = list.layout(constraint, &mut view, cx);
699            assert_eq!(
700                logical_scroll_top,
701                ListOffset {
702                    item_ix: 2,
703                    offset_in_item: 4.
704                }
705            );
706            assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 54.);
707
708            elements.borrow_mut().splice(1..2, vec![(3, 40.), (4, 50.)]);
709            elements.borrow_mut().push((5, 60.));
710            state.splice(1..2, 2);
711            state.splice(4..4, 1);
712            assert_eq!(
713                state.0.borrow().items.summary().clone(),
714                ListItemSummary {
715                    count: 5,
716                    rendered_count: 2,
717                    unrendered_count: 3,
718                    height: 120.
719                }
720            );
721
722            let (size, logical_scroll_top) = list.layout(constraint, &mut view, cx);
723            assert_eq!(size, vec2f(100., 40.));
724            assert_eq!(
725                state.0.borrow().items.summary().clone(),
726                ListItemSummary {
727                    count: 5,
728                    rendered_count: 5,
729                    unrendered_count: 0,
730                    height: 270.
731                }
732            );
733            assert_eq!(
734                logical_scroll_top,
735                ListOffset {
736                    item_ix: 3,
737                    offset_in_item: 4.
738                }
739            );
740            assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.);
741
742            view
743        });
744    }
745
746    #[crate::test(self, iterations = 10)]
747    fn test_random(cx: &mut crate::AppContext, mut rng: StdRng) {
748        let operations = env::var("OPERATIONS")
749            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
750            .unwrap_or(10);
751
752        cx.add_window(Default::default(), |cx| {
753            let mut view = TestView;
754
755            let mut next_id = 0;
756            let elements = Rc::new(RefCell::new(
757                (0..rng.gen_range(0..=20))
758                    .map(|_| {
759                        let id = next_id;
760                        next_id += 1;
761                        (id, rng.gen_range(0..=200) as f32 / 2.0)
762                    })
763                    .collect::<Vec<_>>(),
764            ));
765            let orientation = *[Orientation::Top, Orientation::Bottom]
766                .choose(&mut rng)
767                .unwrap();
768            let overdraw = rng.gen_range(1..=100) as f32;
769
770            let state = ListState::new(elements.borrow().len(), orientation, overdraw, {
771                let elements = elements.clone();
772                move |_, ix, _| {
773                    let (id, height) = elements.borrow()[ix];
774                    TestElement::new(id, height).into_any()
775                }
776            });
777
778            let mut width = rng.gen_range(0..=2000) as f32 / 2.;
779            let mut height = rng.gen_range(0..=2000) as f32 / 2.;
780            log::info!("orientation: {:?}", orientation);
781            log::info!("overdraw: {}", overdraw);
782            log::info!("elements: {:?}", elements.borrow());
783            log::info!("size: ({:?}, {:?})", width, height);
784            log::info!("==================");
785
786            let mut last_logical_scroll_top = None;
787            for _ in 0..operations {
788                match rng.gen_range(0..=100) {
789                    0..=29 if last_logical_scroll_top.is_some() => {
790                        let delta = vec2f(0., rng.gen_range(-overdraw..=overdraw));
791                        log::info!(
792                            "Scrolling by {:?}, previous scroll top: {:?}",
793                            delta,
794                            last_logical_scroll_top.unwrap()
795                        );
796                        state.0.borrow_mut().scroll(
797                            last_logical_scroll_top.as_ref().unwrap(),
798                            height,
799                            delta,
800                            true,
801                            &mut view,
802                            cx,
803                        );
804                    }
805                    30..=34 => {
806                        width = rng.gen_range(0..=2000) as f32 / 2.;
807                        log::info!("changing width: {:?}", width);
808                    }
809                    35..=54 => {
810                        height = rng.gen_range(0..=1000) as f32 / 2.;
811                        log::info!("changing height: {:?}", height);
812                    }
813                    _ => {
814                        let mut elements = elements.borrow_mut();
815                        let end_ix = rng.gen_range(0..=elements.len());
816                        let start_ix = rng.gen_range(0..=end_ix);
817                        let new_elements = (0..rng.gen_range(0..10))
818                            .map(|_| {
819                                let id = next_id;
820                                next_id += 1;
821                                (id, rng.gen_range(0..=200) as f32 / 2.)
822                            })
823                            .collect::<Vec<_>>();
824                        log::info!("splice({:?}, {:?})", start_ix..end_ix, new_elements);
825                        state.splice(start_ix..end_ix, new_elements.len());
826                        elements.splice(start_ix..end_ix, new_elements);
827                        for (ix, item) in state.0.borrow().items.cursor::<()>().enumerate() {
828                            if let ListItem::Rendered(element) = item {
829                                let (expected_id, _) = elements[ix];
830                                element.borrow().with_metadata(|metadata: Option<&usize>| {
831                                    assert_eq!(*metadata.unwrap(), expected_id);
832                                });
833                            }
834                        }
835                    }
836                }
837
838                let mut list = List::new(state.clone());
839                let window_size = vec2f(width, height);
840                let (size, logical_scroll_top) = list.layout(
841                    SizeConstraint::new(vec2f(0., 0.), window_size),
842                    &mut view,
843                    cx,
844                );
845                assert_eq!(size, window_size);
846                last_logical_scroll_top = Some(logical_scroll_top);
847
848                let state = state.0.borrow();
849                log::info!("items {:?}", state.items.items(&()));
850
851                let scroll_top = state.scroll_top(&logical_scroll_top);
852                let rendered_top = (scroll_top - overdraw).max(0.);
853                let rendered_bottom = scroll_top + height + overdraw;
854                let mut item_top = 0.;
855
856                log::info!(
857                    "rendered top {:?}, rendered bottom {:?}, scroll top {:?}",
858                    rendered_top,
859                    rendered_bottom,
860                    scroll_top,
861                );
862
863                let mut first_rendered_element_top = None;
864                let mut last_rendered_element_bottom = None;
865                assert_eq!(state.items.summary().count, elements.borrow().len());
866                for (ix, item) in state.items.cursor::<()>().enumerate() {
867                    match item {
868                        ListItem::Unrendered => {
869                            let item_bottom = item_top;
870                            assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
871                            item_top = item_bottom;
872                        }
873                        ListItem::Removed(height) => {
874                            let (id, expected_height) = elements.borrow()[ix];
875                            assert_eq!(
876                                *height, expected_height,
877                                "element {} height didn't match",
878                                id
879                            );
880                            let item_bottom = item_top + height;
881                            assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
882                            item_top = item_bottom;
883                        }
884                        ListItem::Rendered(element) => {
885                            let (expected_id, expected_height) = elements.borrow()[ix];
886                            let element = element.borrow();
887                            element.with_metadata(|metadata: Option<&usize>| {
888                                assert_eq!(*metadata.unwrap(), expected_id);
889                            });
890                            assert_eq!(element.size().y(), expected_height);
891                            let item_bottom = item_top + element.size().y();
892                            first_rendered_element_top.get_or_insert(item_top);
893                            last_rendered_element_bottom = Some(item_bottom);
894                            assert!(item_bottom > rendered_top || item_top < rendered_bottom);
895                            item_top = item_bottom;
896                        }
897                    }
898                }
899
900                match orientation {
901                    Orientation::Top => {
902                        if let Some(first_rendered_element_top) = first_rendered_element_top {
903                            assert!(first_rendered_element_top <= scroll_top);
904                        }
905                    }
906                    Orientation::Bottom => {
907                        if let Some(last_rendered_element_bottom) = last_rendered_element_bottom {
908                            assert!(last_rendered_element_bottom >= scroll_top + height);
909                        }
910                    }
911                }
912            }
913
914            view
915        });
916    }
917
918    struct TestView;
919
920    impl Entity for TestView {
921        type Event = ();
922    }
923
924    impl crate::View for TestView {
925        fn ui_name() -> &'static str {
926            "TestView"
927        }
928
929        fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
930            Empty::new().into_any()
931        }
932    }
933
934    struct TestElement {
935        id: usize,
936        size: Vector2F,
937    }
938
939    impl TestElement {
940        fn new(id: usize, height: f32) -> Self {
941            Self {
942                id,
943                size: vec2f(100., height),
944            }
945        }
946    }
947
948    impl<V: 'static> Element<V> for TestElement {
949        type LayoutState = ();
950        type PaintState = ();
951
952        fn layout(
953            &mut self,
954            _: SizeConstraint,
955            _: &mut V,
956            _: &mut ViewContext<V>,
957        ) -> (Vector2F, ()) {
958            (self.size, ())
959        }
960
961        fn paint(&mut self, _: RectF, _: RectF, _: &mut (), _: &mut V, _: &mut ViewContext<V>) {
962            unimplemented!()
963        }
964
965        fn rect_for_text_range(
966            &self,
967            _: Range<usize>,
968            _: RectF,
969            _: RectF,
970            _: &Self::LayoutState,
971            _: &Self::PaintState,
972            _: &V,
973            _: &ViewContext<V>,
974        ) -> Option<RectF> {
975            unimplemented!()
976        }
977
978        fn debug(&self, _: RectF, _: &(), _: &(), _: &V, _: &ViewContext<V>) -> serde_json::Value {
979            self.id.into()
980        }
981
982        fn metadata(&self) -> Option<&dyn std::any::Any> {
983            Some(&self.id)
984        }
985    }
986}