list.rs

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