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, PaintContext,
  9    RenderContext, ScrollWheelEvent, 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        cx.scene.push_layer(Some(bounds));
265
266        let state = &mut *self.state.0.borrow_mut();
267        for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
268            element.paint(origin, visible_bounds, cx);
269        }
270
271        cx.scene.pop_layer();
272    }
273
274    fn dispatch_event(
275        &mut self,
276        event: &Event,
277        bounds: RectF,
278        _: RectF,
279        scroll_top: &mut ListOffset,
280        _: &mut (),
281        cx: &mut EventContext,
282    ) -> bool {
283        let mut handled = false;
284
285        let mut state = self.state.0.borrow_mut();
286        let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
287        let mut cursor = state.items.cursor::<Count>();
288        let mut new_items = cursor.slice(&Count(scroll_top.item_ix), Bias::Right, &());
289        while let Some(item) = cursor.item() {
290            if item_origin.y() > bounds.max_y() {
291                break;
292            }
293
294            if let ListItem::Rendered(element) = item {
295                let prev_notify_count = cx.notify_count();
296                let mut element = element.clone();
297                handled = element.dispatch_event(event, cx) || handled;
298                item_origin.set_y(item_origin.y() + element.size().y());
299                if cx.notify_count() > prev_notify_count {
300                    new_items.push(ListItem::Unrendered, &());
301                    self.invalidated_elements.push(element);
302                } else {
303                    new_items.push(item.clone(), &());
304                }
305                cursor.next(&());
306            } else {
307                unreachable!();
308            }
309        }
310
311        new_items.push_tree(cursor.suffix(&()), &());
312        drop(cursor);
313        state.items = new_items;
314
315        if let Event::ScrollWheel(ScrollWheelEvent {
316            position,
317            delta,
318            precise,
319            ..
320        }) = event
321        {
322            if bounds.contains_point(*position)
323                && state.scroll(scroll_top, bounds.height(), *delta, *precise, cx)
324            {
325                handled = true;
326            }
327        }
328
329        handled
330    }
331
332    fn rect_for_text_range(
333        &self,
334        range_utf16: Range<usize>,
335        bounds: RectF,
336        _: RectF,
337        scroll_top: &Self::LayoutState,
338        _: &Self::PaintState,
339        cx: &MeasurementContext,
340    ) -> Option<RectF> {
341        let state = self.state.0.borrow();
342        let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
343        let mut cursor = state.items.cursor::<Count>();
344        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
345        while let Some(item) = cursor.item() {
346            if item_origin.y() > bounds.max_y() {
347                break;
348            }
349
350            if let ListItem::Rendered(element) = item {
351                if let Some(rect) = element.rect_for_text_range(range_utf16.clone(), cx) {
352                    return Some(rect);
353                }
354
355                item_origin.set_y(item_origin.y() + element.size().y());
356                cursor.next(&());
357            } else {
358                unreachable!();
359            }
360        }
361
362        None
363    }
364
365    fn debug(
366        &self,
367        bounds: RectF,
368        scroll_top: &Self::LayoutState,
369        _: &(),
370        cx: &DebugContext,
371    ) -> serde_json::Value {
372        let state = self.state.0.borrow_mut();
373        let visible_elements = state
374            .visible_elements(bounds, scroll_top)
375            .map(|e| e.0.debug(cx))
376            .collect::<Vec<_>>();
377        let visible_range = scroll_top.item_ix..(scroll_top.item_ix + visible_elements.len());
378        json!({
379            "visible_range": visible_range,
380            "visible_elements": visible_elements,
381            "scroll_top": state.logical_scroll_top.map(|top| (top.item_ix, top.offset_in_item)),
382        })
383    }
384}
385
386impl ListState {
387    pub fn new<F, V>(
388        element_count: usize,
389        orientation: Orientation,
390        overdraw: f32,
391        cx: &mut ViewContext<V>,
392        mut render_item: F,
393    ) -> Self
394    where
395        V: View,
396        F: 'static + FnMut(&mut V, usize, &mut RenderContext<V>) -> ElementBox,
397    {
398        let mut items = SumTree::new();
399        items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
400        let handle = cx.weak_handle();
401        Self(Rc::new(RefCell::new(StateInner {
402            last_layout_width: None,
403            render_item: Box::new(move |ix, cx| {
404                let handle = handle.upgrade(cx)?;
405                Some(cx.render(&handle, |view, cx| render_item(view, ix, cx)))
406            }),
407            rendered_range: 0..0,
408            items,
409            logical_scroll_top: None,
410            orientation,
411            overdraw,
412            scroll_handler: None,
413        })))
414    }
415
416    pub fn reset(&self, element_count: usize) {
417        let state = &mut *self.0.borrow_mut();
418        state.rendered_range = 0..0;
419        state.logical_scroll_top = None;
420        state.items = SumTree::new();
421        state
422            .items
423            .extend((0..element_count).map(|_| ListItem::Unrendered), &());
424    }
425
426    pub fn splice(&self, old_range: Range<usize>, count: usize) {
427        let state = &mut *self.0.borrow_mut();
428
429        if let Some(ListOffset {
430            item_ix,
431            offset_in_item,
432        }) = state.logical_scroll_top.as_mut()
433        {
434            if old_range.contains(item_ix) {
435                *item_ix = old_range.start;
436                *offset_in_item = 0.;
437            } else if old_range.end <= *item_ix {
438                *item_ix = *item_ix - (old_range.end - old_range.start) + count;
439            }
440        }
441
442        let new_end = old_range.start + count;
443        if old_range.start < state.rendered_range.start {
444            state.rendered_range.start =
445                new_end + state.rendered_range.start.saturating_sub(old_range.end);
446        }
447        if old_range.start < state.rendered_range.end {
448            state.rendered_range.end =
449                new_end + state.rendered_range.end.saturating_sub(old_range.end);
450        }
451
452        let mut old_heights = state.items.cursor::<Count>();
453        let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
454        old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
455
456        new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
457        new_heights.push_tree(old_heights.suffix(&()), &());
458        drop(old_heights);
459        state.items = new_heights;
460    }
461
462    pub fn set_scroll_handler(
463        &mut self,
464        handler: impl FnMut(Range<usize>, &mut EventContext) + 'static,
465    ) {
466        self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
467    }
468}
469
470impl StateInner {
471    fn render_item(
472        &mut self,
473        ix: usize,
474        existing_element: Option<&ListItem>,
475        constraint: SizeConstraint,
476        cx: &mut LayoutContext,
477    ) -> Option<ElementRc> {
478        if let Some(ListItem::Rendered(element)) = existing_element {
479            Some(element.clone())
480        } else {
481            let mut element = (self.render_item)(ix, cx)?;
482            element.layout(constraint, cx);
483            Some(element.into())
484        }
485    }
486
487    fn visible_range(&self, height: f32, scroll_top: &ListOffset) -> Range<usize> {
488        let mut cursor = self.items.cursor::<ListItemSummary>();
489        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
490        let start_y = cursor.start().height + scroll_top.offset_in_item;
491        cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
492        scroll_top.item_ix..cursor.start().count + 1
493    }
494
495    fn visible_elements<'a>(
496        &'a self,
497        bounds: RectF,
498        scroll_top: &ListOffset,
499    ) -> impl Iterator<Item = (ElementRc, Vector2F)> + 'a {
500        let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
501        let mut cursor = self.items.cursor::<Count>();
502        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
503        std::iter::from_fn(move || {
504            while let Some(item) = cursor.item() {
505                if item_origin.y() > bounds.max_y() {
506                    break;
507                }
508
509                if let ListItem::Rendered(element) = item {
510                    let result = (element.clone(), item_origin);
511                    item_origin.set_y(item_origin.y() + element.size().y());
512                    cursor.next(&());
513                    return Some(result);
514                }
515
516                cursor.next(&());
517            }
518
519            None
520        })
521    }
522
523    fn scroll(
524        &mut self,
525        scroll_top: &ListOffset,
526        height: f32,
527        mut delta: Vector2F,
528        precise: bool,
529        cx: &mut EventContext,
530    ) -> bool {
531        if !precise {
532            delta *= 20.;
533        }
534
535        let scroll_max = (self.items.summary().height - height).max(0.);
536        let new_scroll_top = (self.scroll_top(scroll_top) - delta.y())
537            .max(0.)
538            .min(scroll_max);
539
540        if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max {
541            self.logical_scroll_top = None;
542        } else {
543            let mut cursor = self.items.cursor::<ListItemSummary>();
544            cursor.seek(&Height(new_scroll_top), Bias::Right, &());
545            let item_ix = cursor.start().count;
546            let offset_in_item = new_scroll_top - cursor.start().height;
547            self.logical_scroll_top = Some(ListOffset {
548                item_ix,
549                offset_in_item,
550            });
551        }
552
553        if self.scroll_handler.is_some() {
554            let visible_range = self.visible_range(height, scroll_top);
555            self.scroll_handler.as_mut().unwrap()(visible_range, cx);
556        }
557        cx.notify();
558
559        true
560    }
561
562    fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
563        let mut cursor = self.items.cursor::<ListItemSummary>();
564        cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
565        cursor.start().height + logical_scroll_top.offset_in_item
566    }
567}
568
569impl ListItem {
570    fn remove(&self) -> Self {
571        match self {
572            ListItem::Unrendered => ListItem::Unrendered,
573            ListItem::Rendered(element) => ListItem::Removed(element.size().y()),
574            ListItem::Removed(height) => ListItem::Removed(*height),
575        }
576    }
577}
578
579impl sum_tree::Item for ListItem {
580    type Summary = ListItemSummary;
581
582    fn summary(&self) -> Self::Summary {
583        match self {
584            ListItem::Unrendered => ListItemSummary {
585                count: 1,
586                rendered_count: 0,
587                unrendered_count: 1,
588                height: 0.,
589            },
590            ListItem::Rendered(element) => ListItemSummary {
591                count: 1,
592                rendered_count: 1,
593                unrendered_count: 0,
594                height: element.size().y(),
595            },
596            ListItem::Removed(height) => ListItemSummary {
597                count: 1,
598                rendered_count: 0,
599                unrendered_count: 1,
600                height: *height,
601            },
602        }
603    }
604}
605
606impl sum_tree::Summary for ListItemSummary {
607    type Context = ();
608
609    fn add_summary(&mut self, summary: &Self, _: &()) {
610        self.count += summary.count;
611        self.rendered_count += summary.rendered_count;
612        self.unrendered_count += summary.unrendered_count;
613        self.height += summary.height;
614    }
615}
616
617impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
618    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
619        self.0 += summary.count;
620    }
621}
622
623impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
624    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
625        self.0 += summary.rendered_count;
626    }
627}
628
629impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
630    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
631        self.0 += summary.unrendered_count;
632    }
633}
634
635impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
636    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
637        self.0 += summary.height;
638    }
639}
640
641impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
642    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
643        self.0.partial_cmp(&other.count).unwrap()
644    }
645}
646
647impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
648    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
649        self.0.partial_cmp(&other.height).unwrap()
650    }
651}
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656    use crate::{elements::Empty, geometry::vector::vec2f, Entity};
657    use rand::prelude::*;
658    use std::env;
659
660    #[crate::test(self)]
661    fn test_layout(cx: &mut crate::MutableAppContext) {
662        let mut presenter = cx.build_presenter(0, 0., Default::default());
663        let (_, view) = cx.add_window(Default::default(), |_| TestView);
664        let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
665
666        let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)]));
667
668        let state = view.update(cx, |_, cx| {
669            ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, cx, {
670                let elements = elements.clone();
671                move |_, ix, _| {
672                    let (id, height) = elements.borrow()[ix];
673                    TestElement::new(id, height).boxed()
674                }
675            })
676        });
677
678        let mut list = List::new(state.clone());
679        let (size, _) = list.layout(
680            constraint,
681            &mut presenter.build_layout_context(vec2f(100., 40.), false, cx),
682        );
683        assert_eq!(size, vec2f(100., 40.));
684        assert_eq!(
685            state.0.borrow().items.summary().clone(),
686            ListItemSummary {
687                count: 3,
688                rendered_count: 3,
689                unrendered_count: 0,
690                height: 150.
691            }
692        );
693
694        state.0.borrow_mut().scroll(
695            &ListOffset {
696                item_ix: 0,
697                offset_in_item: 0.,
698            },
699            40.,
700            vec2f(0., -54.),
701            true,
702            &mut presenter.build_event_context(&mut Default::default(), cx),
703        );
704        let (_, logical_scroll_top) = list.layout(
705            constraint,
706            &mut presenter.build_layout_context(vec2f(100., 40.), false, cx),
707        );
708        assert_eq!(
709            logical_scroll_top,
710            ListOffset {
711                item_ix: 2,
712                offset_in_item: 4.
713            }
714        );
715        assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 54.);
716
717        elements.borrow_mut().splice(1..2, vec![(3, 40.), (4, 50.)]);
718        elements.borrow_mut().push((5, 60.));
719        state.splice(1..2, 2);
720        state.splice(4..4, 1);
721        assert_eq!(
722            state.0.borrow().items.summary().clone(),
723            ListItemSummary {
724                count: 5,
725                rendered_count: 2,
726                unrendered_count: 3,
727                height: 120.
728            }
729        );
730
731        let (size, logical_scroll_top) = list.layout(
732            constraint,
733            &mut presenter.build_layout_context(vec2f(100., 40.), false, cx),
734        );
735        assert_eq!(size, vec2f(100., 40.));
736        assert_eq!(
737            state.0.borrow().items.summary().clone(),
738            ListItemSummary {
739                count: 5,
740                rendered_count: 5,
741                unrendered_count: 0,
742                height: 270.
743            }
744        );
745        assert_eq!(
746            logical_scroll_top,
747            ListOffset {
748                item_ix: 3,
749                offset_in_item: 4.
750            }
751        );
752        assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.);
753    }
754
755    #[crate::test(self, iterations = 10, seed = 0)]
756    fn test_random(cx: &mut crate::MutableAppContext, mut rng: StdRng) {
757        let operations = env::var("OPERATIONS")
758            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
759            .unwrap_or(10);
760
761        let (_, view) = cx.add_window(Default::default(), |_| TestView);
762        let mut presenter = cx.build_presenter(0, 0., Default::default());
763        let mut next_id = 0;
764        let elements = Rc::new(RefCell::new(
765            (0..rng.gen_range(0..=20))
766                .map(|_| {
767                    let id = next_id;
768                    next_id += 1;
769                    (id, rng.gen_range(0..=200) as f32 / 2.0)
770                })
771                .collect::<Vec<_>>(),
772        ));
773        let orientation = *[Orientation::Top, Orientation::Bottom]
774            .choose(&mut rng)
775            .unwrap();
776        let overdraw = rng.gen_range(1..=100) as f32;
777
778        let state = view.update(cx, |_, cx| {
779            ListState::new(elements.borrow().len(), orientation, overdraw, cx, {
780                let elements = elements.clone();
781                move |_, ix, _| {
782                    let (id, height) = elements.borrow()[ix];
783                    TestElement::new(id, height).boxed()
784                }
785            })
786        });
787
788        let mut width = rng.gen_range(0..=2000) as f32 / 2.;
789        let mut height = rng.gen_range(0..=2000) as f32 / 2.;
790        log::info!("orientation: {:?}", orientation);
791        log::info!("overdraw: {}", overdraw);
792        log::info!("elements: {:?}", elements.borrow());
793        log::info!("size: ({:?}, {:?})", width, height);
794        log::info!("==================");
795
796        let mut last_logical_scroll_top = None;
797        for _ in 0..operations {
798            match rng.gen_range(0..=100) {
799                0..=29 if last_logical_scroll_top.is_some() => {
800                    let delta = vec2f(0., rng.gen_range(-overdraw..=overdraw));
801                    log::info!(
802                        "Scrolling by {:?}, previous scroll top: {:?}",
803                        delta,
804                        last_logical_scroll_top.unwrap()
805                    );
806                    state.0.borrow_mut().scroll(
807                        last_logical_scroll_top.as_ref().unwrap(),
808                        height,
809                        delta,
810                        true,
811                        &mut presenter.build_event_context(&mut Default::default(), cx),
812                    );
813                }
814                30..=34 => {
815                    width = rng.gen_range(0..=2000) as f32 / 2.;
816                    log::info!("changing width: {:?}", width);
817                }
818                35..=54 => {
819                    height = rng.gen_range(0..=1000) as f32 / 2.;
820                    log::info!("changing height: {:?}", height);
821                }
822                _ => {
823                    let mut elements = elements.borrow_mut();
824                    let end_ix = rng.gen_range(0..=elements.len());
825                    let start_ix = rng.gen_range(0..=end_ix);
826                    let new_elements = (0..rng.gen_range(0..10))
827                        .map(|_| {
828                            let id = next_id;
829                            next_id += 1;
830                            (id, rng.gen_range(0..=200) as f32 / 2.)
831                        })
832                        .collect::<Vec<_>>();
833                    log::info!("splice({:?}, {:?})", start_ix..end_ix, new_elements);
834                    state.splice(start_ix..end_ix, new_elements.len());
835                    elements.splice(start_ix..end_ix, new_elements);
836                    for (ix, item) in state.0.borrow().items.cursor::<()>().enumerate() {
837                        if let ListItem::Rendered(element) = item {
838                            let (expected_id, _) = elements[ix];
839                            element.with_metadata(|metadata: Option<&usize>| {
840                                assert_eq!(*metadata.unwrap(), expected_id);
841                            });
842                        }
843                    }
844                }
845            }
846
847            let mut list = List::new(state.clone());
848            let window_size = vec2f(width, height);
849            let (size, logical_scroll_top) = list.layout(
850                SizeConstraint::new(vec2f(0., 0.), window_size),
851                &mut presenter.build_layout_context(window_size, false, cx),
852            );
853            assert_eq!(size, window_size);
854            last_logical_scroll_top = Some(logical_scroll_top);
855
856            let state = state.0.borrow();
857            log::info!("items {:?}", state.items.items(&()));
858
859            let scroll_top = state.scroll_top(&logical_scroll_top);
860            let rendered_top = (scroll_top - overdraw).max(0.);
861            let rendered_bottom = scroll_top + height + overdraw;
862            let mut item_top = 0.;
863
864            log::info!(
865                "rendered top {:?}, rendered bottom {:?}, scroll top {:?}",
866                rendered_top,
867                rendered_bottom,
868                scroll_top,
869            );
870
871            let mut first_rendered_element_top = None;
872            let mut last_rendered_element_bottom = None;
873            assert_eq!(state.items.summary().count, elements.borrow().len());
874            for (ix, item) in state.items.cursor::<()>().enumerate() {
875                match item {
876                    ListItem::Unrendered => {
877                        let item_bottom = item_top;
878                        assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
879                        item_top = item_bottom;
880                    }
881                    ListItem::Removed(height) => {
882                        let (id, expected_height) = elements.borrow()[ix];
883                        assert_eq!(
884                            *height, expected_height,
885                            "element {} height didn't match",
886                            id
887                        );
888                        let item_bottom = item_top + height;
889                        assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
890                        item_top = item_bottom;
891                    }
892                    ListItem::Rendered(element) => {
893                        let (expected_id, expected_height) = elements.borrow()[ix];
894                        element.with_metadata(|metadata: Option<&usize>| {
895                            assert_eq!(*metadata.unwrap(), expected_id);
896                        });
897                        assert_eq!(element.size().y(), expected_height);
898                        let item_bottom = item_top + element.size().y();
899                        first_rendered_element_top.get_or_insert(item_top);
900                        last_rendered_element_bottom = Some(item_bottom);
901                        assert!(item_bottom > rendered_top || item_top < rendered_bottom);
902                        item_top = item_bottom;
903                    }
904                }
905            }
906
907            match orientation {
908                Orientation::Top => {
909                    if let Some(first_rendered_element_top) = first_rendered_element_top {
910                        assert!(first_rendered_element_top <= scroll_top);
911                    }
912                }
913                Orientation::Bottom => {
914                    if let Some(last_rendered_element_bottom) = last_rendered_element_bottom {
915                        assert!(last_rendered_element_bottom >= scroll_top + height);
916                    }
917                }
918            }
919        }
920    }
921
922    struct TestView;
923
924    impl Entity for TestView {
925        type Event = ();
926    }
927
928    impl View for TestView {
929        fn ui_name() -> &'static str {
930            "TestView"
931        }
932
933        fn render(&mut self, _: &mut RenderContext<'_, Self>) -> ElementBox {
934            Empty::new().boxed()
935        }
936    }
937
938    struct TestElement {
939        id: usize,
940        size: Vector2F,
941    }
942
943    impl TestElement {
944        fn new(id: usize, height: f32) -> Self {
945            Self {
946                id,
947                size: vec2f(100., height),
948            }
949        }
950    }
951
952    impl Element for TestElement {
953        type LayoutState = ();
954        type PaintState = ();
955
956        fn layout(&mut self, _: SizeConstraint, _: &mut LayoutContext) -> (Vector2F, ()) {
957            (self.size, ())
958        }
959
960        fn paint(&mut self, _: RectF, _: RectF, _: &mut (), _: &mut PaintContext) {
961            todo!()
962        }
963
964        fn dispatch_event(
965            &mut self,
966            _: &Event,
967            _: RectF,
968            _: RectF,
969            _: &mut (),
970            _: &mut (),
971            _: &mut EventContext,
972        ) -> bool {
973            todo!()
974        }
975
976        fn rect_for_text_range(
977            &self,
978            _: Range<usize>,
979            _: RectF,
980            _: RectF,
981            _: &Self::LayoutState,
982            _: &Self::PaintState,
983            _: &MeasurementContext,
984        ) -> Option<RectF> {
985            todo!()
986        }
987
988        fn debug(&self, _: RectF, _: &(), _: &(), _: &DebugContext) -> serde_json::Value {
989            self.id.into()
990        }
991
992        fn metadata(&self) -> Option<&dyn std::any::Any> {
993            Some(&self.id)
994        }
995    }
996}