list.rs

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