list.rs

  1use crate::{
  2    geometry::{
  3        rect::RectF,
  4        vector::{vec2f, Vector2F},
  5    },
  6    json::json,
  7    sum_tree::{self, Bias, SumTree},
  8    DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
  9    SizeConstraint,
 10};
 11use std::{cell::RefCell, ops::Range, rc::Rc};
 12
 13pub struct List {
 14    state: ListState,
 15}
 16
 17#[derive(Clone)]
 18pub struct ListState(Rc<RefCell<StateInner>>);
 19
 20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
 21pub enum Orientation {
 22    Top,
 23    Bottom,
 24}
 25
 26struct StateInner {
 27    last_layout_width: Option<f32>,
 28    render_item: Box<dyn FnMut(usize, &mut LayoutContext) -> ElementBox>,
 29    rendered_range: Range<usize>,
 30    items: SumTree<ListItem>,
 31    scroll_top: Option<ScrollTop>,
 32    orientation: Orientation,
 33    overdraw: usize,
 34    scroll_handler: Option<Box<dyn FnMut(Range<usize>, &mut EventContext)>>,
 35}
 36
 37#[derive(Clone, Copy, Debug, Default, PartialEq)]
 38pub struct ScrollTop {
 39    item_ix: usize,
 40    offset_in_item: f32,
 41}
 42
 43#[derive(Clone)]
 44enum ListItem {
 45    Unrendered,
 46    Rendered(ElementRc),
 47    Removed(f32),
 48}
 49
 50impl std::fmt::Debug for ListItem {
 51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 52        match self {
 53            Self::Unrendered => write!(f, "Unrendered"),
 54            Self::Rendered(_) => f.debug_tuple("Rendered").finish(),
 55            Self::Removed(height) => f.debug_tuple("Removed").field(height).finish(),
 56        }
 57    }
 58}
 59
 60#[derive(Clone, Debug, Default, PartialEq)]
 61struct ListItemSummary {
 62    count: usize,
 63    rendered_count: usize,
 64    unrendered_count: usize,
 65    height: f32,
 66}
 67
 68#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
 69struct Count(usize);
 70
 71#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
 72struct RenderedCount(usize);
 73
 74#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
 75struct UnrenderedCount(usize);
 76
 77#[derive(Clone, Debug, Default)]
 78struct Height(f32);
 79
 80impl List {
 81    pub fn new(state: ListState) -> Self {
 82        Self { state }
 83    }
 84}
 85
 86impl Element for List {
 87    type LayoutState = ScrollTop;
 88    type PaintState = ();
 89
 90    fn layout(
 91        &mut self,
 92        constraint: SizeConstraint,
 93        cx: &mut LayoutContext,
 94    ) -> (Vector2F, Self::LayoutState) {
 95        let state = &mut *self.state.0.borrow_mut();
 96        let size = constraint.max;
 97        let mut item_constraint = constraint;
 98        item_constraint.min.set_y(0.);
 99        item_constraint.max.set_y(f32::INFINITY);
100
101        if state.last_layout_width != Some(constraint.max.x()) {
102            state.rendered_range = 0..0;
103            state.items = SumTree::from_iter(
104                (0..state.items.summary().count).map(|_| ListItem::Unrendered),
105                &(),
106            )
107        }
108
109        let overdraw = state.overdraw;
110        let old_rendered_range = state.rendered_range.clone();
111        let old_items = state.items.clone();
112        let orientation = state.orientation;
113        let stored_scroll_top = state.scroll_top;
114        let mut new_items = SumTree::new();
115
116        let mut render_item = |ix, old_item: &ListItem| {
117            let element = if let ListItem::Rendered(element) = old_item {
118                element.clone()
119            } else {
120                let mut element = (state.render_item)(ix, cx);
121                element.layout(item_constraint, cx);
122                element.into()
123            };
124            element
125        };
126
127        // Determine the scroll top. When parked at the end of a bottom-oriented
128        // list, this requires rendering items starting from the end of the list
129        // until the visible region is full. In other cases, the stored scroll
130        // can be used.
131        let scroll_top;
132        let trailing_items;
133        if let (Orientation::Bottom, None) = (orientation, stored_scroll_top) {
134            let mut rendered_height = 0.;
135            let mut cursor = old_items.cursor::<Count, ()>();
136
137            let mut visible_items = Vec::new();
138            cursor.seek(&Count(old_items.summary().count), Bias::Left, &());
139            while let Some(item) = cursor.item() {
140                if rendered_height >= size.y() {
141                    break;
142                }
143
144                let element = render_item(cursor.seek_start().0, item);
145                rendered_height += element.size().y();
146                visible_items.push(ListItem::Rendered(element));
147                cursor.prev(&());
148            }
149
150            scroll_top = ScrollTop {
151                item_ix: cursor.seek_start().0,
152                offset_in_item: rendered_height - size.y(),
153            };
154            visible_items.reverse();
155            trailing_items = Some(visible_items);
156        } else {
157            scroll_top = stored_scroll_top.unwrap_or_default();
158            trailing_items = None;
159        }
160
161        let new_rendered_range_start = scroll_top.item_ix.saturating_sub(overdraw);
162        let mut cursor = old_items.cursor::<Count, ()>();
163
164        // Discard any rendered elements before the overdraw window.
165        if old_rendered_range.start < new_rendered_range_start {
166            new_items.push_tree(
167                cursor.slice(&Count(old_rendered_range.start), Bias::Right, &()),
168                &(),
169            );
170            let remove_to = old_rendered_range.end.min(new_rendered_range_start);
171            while cursor.seek_start().0 < remove_to {
172                new_items.push(cursor.item().unwrap().remove(), &());
173                cursor.next(&());
174            }
175        }
176
177        new_items.push_tree(
178            cursor.slice(&Count(new_rendered_range_start), Bias::Right, &()),
179            &(),
180        );
181
182        // Ensure that all items in the overdraw window before the visible range are rendered.
183        while cursor.seek_start().0 < scroll_top.item_ix {
184            new_items.push(
185                ListItem::Rendered(render_item(cursor.seek_start().0, cursor.item().unwrap())),
186                &(),
187            );
188            cursor.next(&());
189        }
190
191        // The remaining items may have already been rendered, when parked at the
192        // end of a bottom-oriented list. If so, append them.
193        let new_rendered_range_end;
194        if let Some(trailing_items) = trailing_items {
195            new_rendered_range_end = new_rendered_range_start + trailing_items.len();
196            new_items.extend(trailing_items, &());
197        } else {
198            // Ensure that enough items are rendered to fill the visible range.
199            let mut rendered_top = -scroll_top.offset_in_item;
200            while let Some(item) = cursor.item() {
201                if rendered_top >= size.y() {
202                    break;
203                }
204
205                let element = render_item(cursor.seek_start().0, item);
206                rendered_top += element.size().y();
207                new_items.push(ListItem::Rendered(element), &());
208                cursor.next(&());
209            }
210
211            // Ensure that all items in the overdraw window after the visible range
212            // are rendered.
213            new_rendered_range_end =
214                (cursor.seek_start().0 + overdraw).min(old_items.summary().count);
215            while cursor.seek_start().0 < new_rendered_range_end {
216                new_items.push(
217                    ListItem::Rendered(render_item(cursor.seek_start().0, cursor.item().unwrap())),
218                    &(),
219                );
220                cursor.next(&());
221            }
222
223            // Preserve the remainder of the items, but discard any rendered items after
224            // the overdraw window.
225            if cursor.seek_start().0 < old_rendered_range.start {
226                new_items.push_tree(
227                    cursor.slice(&Count(old_rendered_range.start), Bias::Right, &()),
228                    &(),
229                );
230            }
231            while cursor.seek_start().0 < old_rendered_range.end {
232                new_items.push(cursor.item().unwrap().remove(), &());
233                cursor.next(&());
234            }
235            new_items.push_tree(cursor.suffix(&()), &());
236        }
237
238        drop(cursor);
239        state.items = new_items;
240        state.rendered_range = new_rendered_range_start..new_rendered_range_end;
241        state.last_layout_width = Some(size.x());
242        (size, scroll_top)
243    }
244
245    fn paint(&mut self, bounds: RectF, scroll_top: &mut ScrollTop, cx: &mut PaintContext) {
246        cx.scene.push_layer(Some(bounds));
247
248        let state = &mut *self.state.0.borrow_mut();
249        for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
250            element.paint(origin, cx);
251        }
252
253        cx.scene.pop_layer();
254    }
255
256    fn dispatch_event(
257        &mut self,
258        event: &Event,
259        bounds: RectF,
260        scroll_top: &mut ScrollTop,
261        _: &mut (),
262        cx: &mut EventContext,
263    ) -> bool {
264        let mut handled = false;
265
266        let mut state = self.state.0.borrow_mut();
267        for (mut element, _) in state.visible_elements(bounds, scroll_top) {
268            handled = element.dispatch_event(event, cx) || handled;
269        }
270
271        match event {
272            Event::ScrollWheel {
273                position,
274                delta,
275                precise,
276            } => {
277                if bounds.contains_point(*position) {
278                    if state.scroll(scroll_top, bounds.height(), *delta, *precise, cx) {
279                        handled = true;
280                    }
281                }
282            }
283            _ => {}
284        }
285
286        handled
287    }
288
289    fn debug(
290        &self,
291        bounds: RectF,
292        scroll_top: &Self::LayoutState,
293        _: &(),
294        cx: &DebugContext,
295    ) -> serde_json::Value {
296        let state = self.state.0.borrow_mut();
297        let visible_elements = state
298            .visible_elements(bounds, scroll_top)
299            .map(|e| e.0.debug(cx))
300            .collect::<Vec<_>>();
301        let visible_range = scroll_top.item_ix..(scroll_top.item_ix + visible_elements.len());
302        json!({
303            "visible_range": visible_range,
304            "visible_elements": visible_elements,
305            "scroll_top": state.scroll_top.map(|top| (top.item_ix, top.offset_in_item)),
306        })
307    }
308}
309
310impl ListState {
311    pub fn new<F>(
312        element_count: usize,
313        orientation: Orientation,
314        min_overdraw: usize,
315        render_item: F,
316    ) -> Self
317    where
318        F: 'static + FnMut(usize, &mut LayoutContext) -> ElementBox,
319    {
320        let mut items = SumTree::new();
321        items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
322        Self(Rc::new(RefCell::new(StateInner {
323            last_layout_width: None,
324            render_item: Box::new(render_item),
325            rendered_range: 0..0,
326            items,
327            scroll_top: None,
328            orientation,
329            overdraw: min_overdraw,
330            scroll_handler: None,
331        })))
332    }
333
334    pub fn reset(&self, element_count: usize) {
335        let state = &mut *self.0.borrow_mut();
336        state.scroll_top = None;
337        state.items = SumTree::new();
338        state
339            .items
340            .extend((0..element_count).map(|_| ListItem::Unrendered), &());
341    }
342
343    pub fn splice(&self, old_range: Range<usize>, count: usize) {
344        let state = &mut *self.0.borrow_mut();
345
346        if let Some(ScrollTop {
347            item_ix,
348            offset_in_item,
349        }) = state.scroll_top.as_mut()
350        {
351            if old_range.contains(item_ix) {
352                *item_ix = old_range.start;
353                *offset_in_item = 0.;
354            } else if old_range.end <= *item_ix {
355                *item_ix = *item_ix - (old_range.end - old_range.start) + count;
356            }
357        }
358
359        let new_end = old_range.start + count;
360        if old_range.start < state.rendered_range.start {
361            state.rendered_range.start =
362                new_end + state.rendered_range.start.saturating_sub(old_range.end);
363        }
364        if old_range.start < state.rendered_range.end {
365            state.rendered_range.end =
366                new_end + state.rendered_range.end.saturating_sub(old_range.end);
367        }
368
369        let mut old_heights = state.items.cursor::<Count, ()>();
370        let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
371        old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
372
373        new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
374        new_heights.push_tree(old_heights.suffix(&()), &());
375        drop(old_heights);
376        state.items = new_heights;
377    }
378
379    pub fn set_scroll_handler(
380        &mut self,
381        handler: impl FnMut(Range<usize>, &mut EventContext) + 'static,
382    ) {
383        self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
384    }
385}
386
387impl StateInner {
388    fn visible_range(&self, height: f32, scroll_top: &ScrollTop) -> Range<usize> {
389        let mut cursor = self.items.cursor::<Count, Height>();
390        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
391        let start_y = cursor.sum_start().0 + scroll_top.offset_in_item;
392        let mut cursor = cursor.swap_dimensions();
393        cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
394        scroll_top.item_ix..cursor.sum_start().0 + 1
395    }
396
397    fn visible_elements<'a>(
398        &'a self,
399        bounds: RectF,
400        scroll_top: &ScrollTop,
401    ) -> impl Iterator<Item = (ElementRc, Vector2F)> + 'a {
402        let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
403        let mut cursor = self.items.cursor::<Count, ()>();
404        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
405        std::iter::from_fn(move || {
406            while let Some(item) = cursor.item() {
407                if item_origin.y() > bounds.max_y() {
408                    break;
409                }
410
411                if let ListItem::Rendered(element) = item {
412                    let result = (element.clone(), item_origin);
413                    item_origin.set_y(item_origin.y() + element.size().y());
414                    cursor.next(&());
415                    return Some(result);
416                }
417
418                cursor.next(&());
419            }
420
421            None
422        })
423    }
424
425    fn scroll(
426        &mut self,
427        scroll_top: &ScrollTop,
428        height: f32,
429        mut delta: Vector2F,
430        precise: bool,
431        cx: &mut EventContext,
432    ) -> bool {
433        if !precise {
434            delta *= 20.;
435        }
436
437        let scroll_max = (self.items.summary().height - height).max(0.);
438        let new_scroll_top = (self.scroll_top(height) + delta.y())
439            .max(0.)
440            .min(scroll_max);
441
442        if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max {
443            self.scroll_top = None;
444        } else {
445            let mut cursor = self.items.cursor::<Height, Count>();
446            cursor.seek(&Height(new_scroll_top), Bias::Right, &());
447            let item_ix = cursor.sum_start().0;
448            let offset_in_item = new_scroll_top - cursor.seek_start().0;
449            self.scroll_top = Some(ScrollTop {
450                item_ix,
451                offset_in_item,
452            });
453        }
454
455        if self.scroll_handler.is_some() {
456            let visible_range = self.visible_range(height, scroll_top);
457            self.scroll_handler.as_mut().unwrap()(visible_range, cx);
458        }
459        cx.notify();
460
461        true
462    }
463
464    fn scroll_top(&self, height: f32) -> f32 {
465        let scroll_max = (self.items.summary().height - height).max(0.);
466        if let Some(ScrollTop {
467            item_ix,
468            offset_in_item,
469        }) = self.scroll_top
470        {
471            let mut cursor = self.items.cursor::<Count, Height>();
472            cursor.seek(&Count(item_ix), Bias::Right, &());
473            (cursor.sum_start().0 + offset_in_item).min(scroll_max)
474        } else {
475            match self.orientation {
476                Orientation::Top => 0.,
477                Orientation::Bottom => scroll_max,
478            }
479        }
480    }
481}
482
483impl ListItem {
484    fn remove(&self) -> Self {
485        match self {
486            ListItem::Unrendered => ListItem::Unrendered,
487            ListItem::Rendered(element) => ListItem::Removed(element.size().y()),
488            ListItem::Removed(height) => ListItem::Removed(*height),
489        }
490    }
491}
492
493impl sum_tree::Item for ListItem {
494    type Summary = ListItemSummary;
495
496    fn summary(&self) -> Self::Summary {
497        match self {
498            ListItem::Unrendered => ListItemSummary {
499                count: 1,
500                rendered_count: 0,
501                unrendered_count: 1,
502                height: 0.,
503            },
504            ListItem::Rendered(element) => ListItemSummary {
505                count: 1,
506                rendered_count: 1,
507                unrendered_count: 0,
508                height: element.size().y(),
509            },
510            ListItem::Removed(height) => ListItemSummary {
511                count: 1,
512                rendered_count: 0,
513                unrendered_count: 1,
514                height: *height,
515            },
516        }
517    }
518}
519
520impl sum_tree::Summary for ListItemSummary {
521    type Context = ();
522
523    fn add_summary(&mut self, summary: &Self, _: &()) {
524        self.count += summary.count;
525        self.rendered_count += summary.rendered_count;
526        self.unrendered_count += summary.unrendered_count;
527        self.height += summary.height;
528    }
529}
530
531impl<'a> sum_tree::Dimension<'a, ListItemSummary> for ListItemSummary {
532    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
533        sum_tree::Summary::add_summary(self, summary, &());
534    }
535}
536
537impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
538    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
539        self.0 += summary.count;
540    }
541}
542
543impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
544    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
545        self.0 += summary.rendered_count;
546    }
547}
548
549impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
550    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
551        self.0 += summary.unrendered_count;
552    }
553}
554
555impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
556    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
557        self.0 += summary.height;
558    }
559}
560
561impl<'a> sum_tree::SeekDimension<'a, ListItemSummary> for Height {
562    fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
563        self.0.partial_cmp(&other.0).unwrap()
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use crate::{
571        elements::{ConstrainedBox, Empty},
572        geometry::vector::vec2f,
573        Entity, RenderContext, View,
574    };
575    use rand::prelude::*;
576    use std::env;
577
578    #[crate::test(self)]
579    fn test_layout(cx: &mut crate::MutableAppContext) {
580        let mut presenter = cx.build_presenter(0, 0.);
581
582        let elements = Rc::new(RefCell::new(vec![20., 30., 100.]));
583        let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000, {
584            let elements = elements.clone();
585            move |ix, _| item(elements.borrow()[ix])
586        });
587
588        let mut list = List::new(state.clone()).boxed();
589        let size = list.layout(
590            SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)),
591            &mut presenter.build_layout_context(cx),
592        );
593        assert_eq!(size, vec2f(100., 40.));
594        assert_eq!(
595            state.0.borrow().items.summary(),
596            ListItemSummary {
597                count: 3,
598                rendered_count: 3,
599                unrendered_count: 0,
600                height: 150.
601            }
602        );
603
604        state.0.borrow_mut().scroll(
605            &ScrollTop {
606                item_ix: 0,
607                offset_in_item: 0.,
608            },
609            40.,
610            vec2f(0., 54.),
611            true,
612            &mut presenter.build_event_context(cx),
613        );
614        assert_eq!(
615            state.0.borrow().scroll_top,
616            Some(ScrollTop {
617                item_ix: 2,
618                offset_in_item: 4.
619            })
620        );
621        assert_eq!(state.0.borrow().scroll_top(size.y()), 54.);
622
623        elements.borrow_mut().splice(1..2, vec![40., 50.]);
624        elements.borrow_mut().push(60.);
625        state.splice(1..2, 2);
626        state.splice(4..4, 1);
627        assert_eq!(
628            state.0.borrow().items.summary(),
629            ListItemSummary {
630                count: 5,
631                rendered_count: 2,
632                unrendered_count: 3,
633                height: 120.
634            }
635        );
636
637        let mut list = List::new(state.clone()).boxed();
638        let size = list.layout(
639            SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)),
640            &mut presenter.build_layout_context(cx),
641        );
642        assert_eq!(size, vec2f(100., 40.));
643        assert_eq!(
644            state.0.borrow().items.summary(),
645            ListItemSummary {
646                count: 5,
647                rendered_count: 5,
648                unrendered_count: 0,
649                height: 270.
650            }
651        );
652        assert_eq!(
653            state.0.borrow().scroll_top,
654            Some(ScrollTop {
655                item_ix: 3,
656                offset_in_item: 4.
657            })
658        );
659        assert_eq!(state.0.borrow().scroll_top(size.y()), 114.);
660    }
661
662    #[crate::test(self, iterations = 10000, seed = 2515)]
663    fn test_random(cx: &mut crate::MutableAppContext, mut rng: StdRng) {
664        let operations = env::var("OPERATIONS")
665            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
666            .unwrap_or(10);
667
668        let mut presenter = cx.build_presenter(0, 0.);
669        let elements = Rc::new(RefCell::new(
670            (0..rng.gen_range(0..=20))
671                .map(|_| rng.gen_range(0_f32..=100_f32))
672                .collect::<Vec<_>>(),
673        ));
674        let orientation = *[Orientation::Top, Orientation::Bottom]
675            .choose(&mut rng)
676            .unwrap();
677        let min_overdraw = rng.gen_range(0..=20);
678        let state = ListState::new(elements.borrow().len(), orientation, min_overdraw, {
679            let elements = elements.clone();
680            move |ix, _| item(elements.borrow()[ix])
681        });
682
683        let mut width = rng.gen_range(0_f32..=1000_f32);
684        let mut height = rng.gen_range(0_f32..=1000_f32);
685        log::info!("orientation: {:?}", orientation);
686        log::info!("min_overdraw: {}", min_overdraw);
687        log::info!("elements: {:?}", elements.borrow());
688        log::info!("size: ({:?}, {:?})", width, height);
689        log::info!("==================");
690
691        let mut scroll_top = None;
692        for _ in 0..operations {
693            match rng.gen_range(0..=100) {
694                0..=29 if scroll_top.is_some() => {
695                    let delta = vec2f(0., rng.gen_range(-100_f32..=100_f32));
696                    log::info!(
697                        "Scrolling by {:?}, previous scroll top: {:?}",
698                        delta,
699                        scroll_top.unwrap()
700                    );
701                    state.0.borrow_mut().scroll(
702                        scroll_top.as_ref().unwrap(),
703                        height,
704                        delta,
705                        true,
706                        &mut presenter.build_event_context(cx),
707                    );
708                }
709                30..=34 => {
710                    width = rng.gen_range(0_f32..=1000_f32);
711                    log::info!("changing width: {:?}", width);
712                }
713                35..=54 => {
714                    height = rng.gen_range(0_f32..=1000_f32);
715                    log::info!("changing height: {:?}", height);
716                }
717                _ => {
718                    let mut elements = elements.borrow_mut();
719                    let end_ix = rng.gen_range(0..=elements.len());
720                    let start_ix = rng.gen_range(0..=end_ix);
721                    let new_elements = (0..rng.gen_range(0..10))
722                        .map(|_| rng.gen_range(0_f32..=100_f32))
723                        .collect::<Vec<_>>();
724                    log::info!("splice({:?}, {:?})", start_ix..end_ix, new_elements);
725                    state.splice(start_ix..end_ix, new_elements.len());
726                    elements.splice(start_ix..end_ix, new_elements);
727                }
728            }
729
730            let mut list = List::new(state.clone());
731            let (size, new_scroll_top) = list.layout(
732                SizeConstraint::new(vec2f(0., 0.), vec2f(width, height)),
733                &mut presenter.build_layout_context(cx),
734            );
735            assert_eq!(size, vec2f(width, height));
736            scroll_top = Some(new_scroll_top);
737
738            let state = state.0.borrow();
739            let visible_range = state.visible_range(height, &new_scroll_top);
740            let rendered_range =
741                visible_range.start.saturating_sub(min_overdraw)..visible_range.end + min_overdraw;
742            log::info!("visible range {:?}", visible_range);
743            log::info!("items {:?}", state.items.items(&()));
744            for (ix, item) in state.items.cursor::<Count, ()>().enumerate() {
745                if rendered_range.contains(&ix) {
746                    assert!(
747                        matches!(item, ListItem::Rendered(_)),
748                        "item {:?} was not rendered",
749                        ix
750                    );
751                } else {
752                    assert!(
753                        !matches!(item, ListItem::Rendered(_)),
754                        "item {:?} was incorrectly rendered",
755                        ix
756                    );
757                }
758            }
759        }
760    }
761
762    fn item(height: f32) -> ElementBox {
763        ConstrainedBox::new(Empty::new().boxed())
764            .with_height(height)
765            .with_width(100.)
766            .boxed()
767    }
768
769    struct TestView;
770
771    impl Entity for TestView {
772        type Event = ();
773    }
774
775    impl View for TestView {
776        fn ui_name() -> &'static str {
777            "TestView"
778        }
779
780        fn render(&mut self, _: &mut RenderContext<'_, Self>) -> ElementBox {
781            unimplemented!()
782        }
783    }
784}