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, collections::VecDeque, 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    logical_scroll_top: Option<ListOffset>,
 32    orientation: Orientation,
 33    overdraw: f32,
 34    scroll_handler: Option<Box<dyn FnMut(Range<usize>, &mut EventContext)>>,
 35}
 36
 37#[derive(Clone, Copy, Debug, Default, PartialEq)]
 38pub struct ListOffset {
 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 = ListOffset;
 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 cx.refreshing || state.last_layout_width != Some(size.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 old_items = state.items.clone();
110        let mut new_items = SumTree::new();
111        let mut rendered_items = VecDeque::new();
112        let mut rendered_height = 0.;
113        let mut scroll_top = state
114            .logical_scroll_top
115            .unwrap_or_else(|| match state.orientation {
116                Orientation::Top => ListOffset {
117                    item_ix: 0,
118                    offset_in_item: 0.,
119                },
120                Orientation::Bottom => ListOffset {
121                    item_ix: state.items.summary().count,
122                    offset_in_item: 0.,
123                },
124            });
125
126        // Render items after the scroll top, including those in the trailing overdraw.
127        let mut cursor = old_items.cursor::<Count, ()>();
128        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
129        for (ix, item) in cursor.by_ref().enumerate() {
130            if rendered_height - scroll_top.offset_in_item >= size.y() + state.overdraw {
131                break;
132            }
133
134            let element = state.render_item(scroll_top.item_ix + ix, item, item_constraint, cx);
135            rendered_height += element.size().y();
136            rendered_items.push_back(ListItem::Rendered(element));
137        }
138
139        // Prepare to start walking upward from the item at the scroll top.
140        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
141
142        // If the rendered items do not fill the visible region, then adjust
143        // the scroll top upward.
144        if rendered_height - scroll_top.offset_in_item < size.y() {
145            while rendered_height < size.y() {
146                cursor.prev(&());
147                if let Some(item) = cursor.item() {
148                    let element =
149                        state.render_item(cursor.seek_start().0, item, item_constraint, cx);
150                    rendered_height += element.size().y();
151                    rendered_items.push_front(ListItem::Rendered(element));
152                } else {
153                    break;
154                }
155            }
156
157            scroll_top = ListOffset {
158                item_ix: cursor.seek_start().0,
159                offset_in_item: rendered_height - size.y(),
160            };
161
162            match state.orientation {
163                Orientation::Top => {
164                    scroll_top.offset_in_item = scroll_top.offset_in_item.max(0.);
165                    state.logical_scroll_top = Some(scroll_top);
166                }
167                Orientation::Bottom => {
168                    scroll_top = ListOffset {
169                        item_ix: cursor.seek_start().0,
170                        offset_in_item: rendered_height - size.y(),
171                    };
172                    state.logical_scroll_top = None;
173                }
174            };
175        }
176
177        // Render items in the leading overdraw.
178        let mut leading_overdraw = scroll_top.offset_in_item;
179        while leading_overdraw < state.overdraw {
180            cursor.prev(&());
181            if let Some(item) = cursor.item() {
182                let element = state.render_item(cursor.seek_start().0, item, item_constraint, cx);
183                leading_overdraw += element.size().y();
184                rendered_items.push_front(ListItem::Rendered(element));
185            } else {
186                break;
187            }
188        }
189
190        let new_rendered_range =
191            cursor.seek_start().0..(cursor.seek_start().0 + rendered_items.len());
192
193        let mut cursor = old_items.cursor::<Count, ()>();
194
195        if state.rendered_range.start < new_rendered_range.start {
196            new_items.push_tree(
197                cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
198                &(),
199            );
200            let remove_to = state.rendered_range.end.min(new_rendered_range.start);
201            while cursor.seek_start().0 < remove_to {
202                new_items.push(cursor.item().unwrap().remove(), &());
203                cursor.next(&());
204            }
205        }
206        new_items.push_tree(
207            cursor.slice(&Count(new_rendered_range.start), Bias::Right, &()),
208            &(),
209        );
210
211        new_items.extend(rendered_items, &());
212        cursor.seek(&Count(new_rendered_range.end), Bias::Right, &());
213
214        if new_rendered_range.end < state.rendered_range.start {
215            new_items.push_tree(
216                cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
217                &(),
218            );
219        }
220        while cursor.seek_start().0 < state.rendered_range.end {
221            new_items.push(cursor.item().unwrap().remove(), &());
222            cursor.next(&());
223        }
224
225        new_items.push_tree(cursor.suffix(&()), &());
226
227        state.items = new_items;
228        state.rendered_range = new_rendered_range;
229        state.last_layout_width = Some(size.x());
230        (size, scroll_top)
231    }
232
233    fn paint(
234        &mut self,
235        bounds: RectF,
236        visible_bounds: RectF,
237        scroll_top: &mut ListOffset,
238        cx: &mut PaintContext,
239    ) {
240        cx.scene.push_layer(Some(bounds));
241
242        let state = &mut *self.state.0.borrow_mut();
243        for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
244            element.paint(origin, visible_bounds, cx);
245        }
246
247        cx.scene.pop_layer();
248    }
249
250    fn dispatch_event(
251        &mut self,
252        event: &Event,
253        bounds: RectF,
254        scroll_top: &mut ListOffset,
255        _: &mut (),
256        cx: &mut EventContext,
257    ) -> bool {
258        let mut handled = false;
259
260        let mut state = self.state.0.borrow_mut();
261        for (mut element, _) in state.visible_elements(bounds, scroll_top) {
262            handled = element.dispatch_event(event, cx) || handled;
263        }
264
265        match event {
266            Event::ScrollWheel {
267                position,
268                delta,
269                precise,
270            } => {
271                if bounds.contains_point(*position) {
272                    if state.scroll(scroll_top, bounds.height(), *delta, *precise, cx) {
273                        handled = true;
274                    }
275                }
276            }
277            _ => {}
278        }
279
280        handled
281    }
282
283    fn debug(
284        &self,
285        bounds: RectF,
286        scroll_top: &Self::LayoutState,
287        _: &(),
288        cx: &DebugContext,
289    ) -> serde_json::Value {
290        let state = self.state.0.borrow_mut();
291        let visible_elements = state
292            .visible_elements(bounds, scroll_top)
293            .map(|e| e.0.debug(cx))
294            .collect::<Vec<_>>();
295        let visible_range = scroll_top.item_ix..(scroll_top.item_ix + visible_elements.len());
296        json!({
297            "visible_range": visible_range,
298            "visible_elements": visible_elements,
299            "scroll_top": state.logical_scroll_top.map(|top| (top.item_ix, top.offset_in_item)),
300        })
301    }
302}
303
304impl ListState {
305    pub fn new<F>(
306        element_count: usize,
307        orientation: Orientation,
308        overdraw: f32,
309        render_item: F,
310    ) -> Self
311    where
312        F: 'static + FnMut(usize, &mut LayoutContext) -> ElementBox,
313    {
314        let mut items = SumTree::new();
315        items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
316        Self(Rc::new(RefCell::new(StateInner {
317            last_layout_width: None,
318            render_item: Box::new(render_item),
319            rendered_range: 0..0,
320            items,
321            logical_scroll_top: None,
322            orientation,
323            overdraw,
324            scroll_handler: None,
325        })))
326    }
327
328    pub fn reset(&self, element_count: usize) {
329        let state = &mut *self.0.borrow_mut();
330        state.rendered_range = 0..0;
331        state.logical_scroll_top = None;
332        state.items = SumTree::new();
333        state
334            .items
335            .extend((0..element_count).map(|_| ListItem::Unrendered), &());
336    }
337
338    pub fn splice(&self, old_range: Range<usize>, count: usize) {
339        let state = &mut *self.0.borrow_mut();
340
341        if let Some(ListOffset {
342            item_ix,
343            offset_in_item,
344        }) = state.logical_scroll_top.as_mut()
345        {
346            if old_range.contains(item_ix) {
347                *item_ix = old_range.start;
348                *offset_in_item = 0.;
349            } else if old_range.end <= *item_ix {
350                *item_ix = *item_ix - (old_range.end - old_range.start) + count;
351            }
352        }
353
354        let new_end = old_range.start + count;
355        if old_range.start < state.rendered_range.start {
356            state.rendered_range.start =
357                new_end + state.rendered_range.start.saturating_sub(old_range.end);
358        }
359        if old_range.start < state.rendered_range.end {
360            state.rendered_range.end =
361                new_end + state.rendered_range.end.saturating_sub(old_range.end);
362        }
363
364        let mut old_heights = state.items.cursor::<Count, ()>();
365        let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
366        old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
367
368        new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
369        new_heights.push_tree(old_heights.suffix(&()), &());
370        drop(old_heights);
371        state.items = new_heights;
372    }
373
374    pub fn set_scroll_handler(
375        &mut self,
376        handler: impl FnMut(Range<usize>, &mut EventContext) + 'static,
377    ) {
378        self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
379    }
380}
381
382impl StateInner {
383    fn render_item(
384        &mut self,
385        ix: usize,
386        existing_item: &ListItem,
387        constraint: SizeConstraint,
388        cx: &mut LayoutContext,
389    ) -> ElementRc {
390        if let ListItem::Rendered(element) = existing_item {
391            element.clone()
392        } else {
393            let mut element = (self.render_item)(ix, cx);
394            element.layout(constraint, cx);
395            element.into()
396        }
397    }
398
399    fn visible_range(&self, height: f32, scroll_top: &ListOffset) -> Range<usize> {
400        let mut cursor = self.items.cursor::<Count, Height>();
401        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
402        let start_y = cursor.sum_start().0 + scroll_top.offset_in_item;
403        let mut cursor = cursor.swap_dimensions();
404        cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
405        scroll_top.item_ix..cursor.sum_start().0 + 1
406    }
407
408    fn visible_elements<'a>(
409        &'a self,
410        bounds: RectF,
411        scroll_top: &ListOffset,
412    ) -> impl Iterator<Item = (ElementRc, Vector2F)> + 'a {
413        let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
414        let mut cursor = self.items.cursor::<Count, ()>();
415        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
416        std::iter::from_fn(move || {
417            while let Some(item) = cursor.item() {
418                if item_origin.y() > bounds.max_y() {
419                    break;
420                }
421
422                if let ListItem::Rendered(element) = item {
423                    let result = (element.clone(), item_origin);
424                    item_origin.set_y(item_origin.y() + element.size().y());
425                    cursor.next(&());
426                    return Some(result);
427                }
428
429                cursor.next(&());
430            }
431
432            None
433        })
434    }
435
436    fn scroll(
437        &mut self,
438        scroll_top: &ListOffset,
439        height: f32,
440        mut delta: Vector2F,
441        precise: bool,
442        cx: &mut EventContext,
443    ) -> bool {
444        if !precise {
445            delta *= 20.;
446        }
447
448        let scroll_max = (self.items.summary().height - height).max(0.);
449        let new_scroll_top = (self.scroll_top(scroll_top) - delta.y())
450            .max(0.)
451            .min(scroll_max);
452
453        if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max {
454            self.logical_scroll_top = None;
455        } else {
456            let mut cursor = self.items.cursor::<Height, Count>();
457            cursor.seek(&Height(new_scroll_top), Bias::Right, &());
458            let item_ix = cursor.sum_start().0;
459            let offset_in_item = new_scroll_top - cursor.seek_start().0;
460            self.logical_scroll_top = Some(ListOffset {
461                item_ix,
462                offset_in_item,
463            });
464        }
465
466        if self.scroll_handler.is_some() {
467            let visible_range = self.visible_range(height, scroll_top);
468            self.scroll_handler.as_mut().unwrap()(visible_range, cx);
469        }
470        cx.notify();
471
472        true
473    }
474
475    fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
476        let mut cursor = self.items.cursor::<Count, Height>();
477        cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
478        cursor.sum_start().0 + logical_scroll_top.offset_in_item
479    }
480}
481
482impl ListItem {
483    fn remove(&self) -> Self {
484        match self {
485            ListItem::Unrendered => ListItem::Unrendered,
486            ListItem::Rendered(element) => ListItem::Removed(element.size().y()),
487            ListItem::Removed(height) => ListItem::Removed(*height),
488        }
489    }
490}
491
492impl sum_tree::Item for ListItem {
493    type Summary = ListItemSummary;
494
495    fn summary(&self) -> Self::Summary {
496        match self {
497            ListItem::Unrendered => ListItemSummary {
498                count: 1,
499                rendered_count: 0,
500                unrendered_count: 1,
501                height: 0.,
502            },
503            ListItem::Rendered(element) => ListItemSummary {
504                count: 1,
505                rendered_count: 1,
506                unrendered_count: 0,
507                height: element.size().y(),
508            },
509            ListItem::Removed(height) => ListItemSummary {
510                count: 1,
511                rendered_count: 0,
512                unrendered_count: 1,
513                height: *height,
514            },
515        }
516    }
517}
518
519impl sum_tree::Summary for ListItemSummary {
520    type Context = ();
521
522    fn add_summary(&mut self, summary: &Self, _: &()) {
523        self.count += summary.count;
524        self.rendered_count += summary.rendered_count;
525        self.unrendered_count += summary.unrendered_count;
526        self.height += summary.height;
527    }
528}
529
530impl<'a> sum_tree::Dimension<'a, ListItemSummary> for ListItemSummary {
531    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
532        sum_tree::Summary::add_summary(self, summary, &());
533    }
534}
535
536impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
537    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
538        self.0 += summary.count;
539    }
540}
541
542impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
543    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
544        self.0 += summary.rendered_count;
545    }
546}
547
548impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
549    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
550        self.0 += summary.unrendered_count;
551    }
552}
553
554impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
555    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
556        self.0 += summary.height;
557    }
558}
559
560impl<'a> sum_tree::SeekDimension<'a, ListItemSummary> for Height {
561    fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
562        self.0.partial_cmp(&other.0).unwrap()
563    }
564}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569    use crate::geometry::vector::vec2f;
570    use rand::prelude::*;
571    use std::env;
572
573    #[crate::test(self)]
574    fn test_layout(cx: &mut crate::MutableAppContext) {
575        let mut presenter = cx.build_presenter(0, 0.);
576        let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
577
578        let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)]));
579        let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, {
580            let elements = elements.clone();
581            move |ix, _| {
582                let (id, height) = elements.borrow()[ix];
583                TestElement::new(id, height).boxed()
584            }
585        });
586
587        let mut list = List::new(state.clone());
588        let (size, _) = list.layout(constraint, &mut presenter.build_layout_context(false, cx));
589        assert_eq!(size, vec2f(100., 40.));
590        assert_eq!(
591            state.0.borrow().items.summary(),
592            ListItemSummary {
593                count: 3,
594                rendered_count: 3,
595                unrendered_count: 0,
596                height: 150.
597            }
598        );
599
600        state.0.borrow_mut().scroll(
601            &ListOffset {
602                item_ix: 0,
603                offset_in_item: 0.,
604            },
605            40.,
606            vec2f(0., -54.),
607            true,
608            &mut presenter.build_event_context(cx),
609        );
610        let (_, logical_scroll_top) =
611            list.layout(constraint, &mut presenter.build_layout_context(false, cx));
612        assert_eq!(
613            logical_scroll_top,
614            ListOffset {
615                item_ix: 2,
616                offset_in_item: 4.
617            }
618        );
619        assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 54.);
620
621        elements.borrow_mut().splice(1..2, vec![(3, 40.), (4, 50.)]);
622        elements.borrow_mut().push((5, 60.));
623        state.splice(1..2, 2);
624        state.splice(4..4, 1);
625        assert_eq!(
626            state.0.borrow().items.summary(),
627            ListItemSummary {
628                count: 5,
629                rendered_count: 2,
630                unrendered_count: 3,
631                height: 120.
632            }
633        );
634
635        let (size, logical_scroll_top) =
636            list.layout(constraint, &mut presenter.build_layout_context(false, cx));
637        assert_eq!(size, vec2f(100., 40.));
638        assert_eq!(
639            state.0.borrow().items.summary(),
640            ListItemSummary {
641                count: 5,
642                rendered_count: 5,
643                unrendered_count: 0,
644                height: 270.
645            }
646        );
647        assert_eq!(
648            logical_scroll_top,
649            ListOffset {
650                item_ix: 3,
651                offset_in_item: 4.
652            }
653        );
654        assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.);
655    }
656
657    #[crate::test(self, iterations = 10, seed = 0)]
658    fn test_random(cx: &mut crate::MutableAppContext, mut rng: StdRng) {
659        let operations = env::var("OPERATIONS")
660            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
661            .unwrap_or(10);
662
663        let mut presenter = cx.build_presenter(0, 0.);
664        let mut next_id = 0;
665        let elements = Rc::new(RefCell::new(
666            (0..rng.gen_range(0..=20))
667                .map(|_| {
668                    let id = next_id;
669                    next_id += 1;
670                    (id, rng.gen_range(0..=200) as f32 / 2.0)
671                })
672                .collect::<Vec<_>>(),
673        ));
674        let orientation = *[Orientation::Top, Orientation::Bottom]
675            .choose(&mut rng)
676            .unwrap();
677        let overdraw = rng.gen_range(1..=100) as f32;
678        let state = ListState::new(elements.borrow().len(), orientation, overdraw, {
679            let elements = elements.clone();
680            move |ix, _| {
681                let (id, height) = elements.borrow()[ix];
682                TestElement::new(id, height).boxed()
683            }
684        });
685
686        let mut width = rng.gen_range(0..=2000) as f32 / 2.;
687        let mut height = rng.gen_range(0..=2000) as f32 / 2.;
688        log::info!("orientation: {:?}", orientation);
689        log::info!("overdraw: {}", overdraw);
690        log::info!("elements: {:?}", elements.borrow());
691        log::info!("size: ({:?}, {:?})", width, height);
692        log::info!("==================");
693
694        let mut last_logical_scroll_top = None;
695        for _ in 0..operations {
696            match rng.gen_range(0..=100) {
697                0..=29 if last_logical_scroll_top.is_some() => {
698                    let delta = vec2f(0., rng.gen_range(-overdraw..=overdraw));
699                    log::info!(
700                        "Scrolling by {:?}, previous scroll top: {:?}",
701                        delta,
702                        last_logical_scroll_top.unwrap()
703                    );
704                    state.0.borrow_mut().scroll(
705                        last_logical_scroll_top.as_ref().unwrap(),
706                        height,
707                        delta,
708                        true,
709                        &mut presenter.build_event_context(cx),
710                    );
711                }
712                30..=34 => {
713                    width = rng.gen_range(0..=2000) as f32 / 2.;
714                    log::info!("changing width: {:?}", width);
715                }
716                35..=54 => {
717                    height = rng.gen_range(0..=1000) as f32 / 2.;
718                    log::info!("changing height: {:?}", height);
719                }
720                _ => {
721                    let mut elements = elements.borrow_mut();
722                    let end_ix = rng.gen_range(0..=elements.len());
723                    let start_ix = rng.gen_range(0..=end_ix);
724                    let new_elements = (0..rng.gen_range(0..10))
725                        .map(|_| {
726                            let id = next_id;
727                            next_id += 1;
728                            (id, rng.gen_range(0..=200) as f32 / 2.)
729                        })
730                        .collect::<Vec<_>>();
731                    log::info!("splice({:?}, {:?})", start_ix..end_ix, new_elements);
732                    state.splice(start_ix..end_ix, new_elements.len());
733                    elements.splice(start_ix..end_ix, new_elements);
734                    for (ix, item) in state.0.borrow().items.cursor::<(), ()>().enumerate() {
735                        if let ListItem::Rendered(element) = item {
736                            let (expected_id, _) = elements[ix];
737                            element.with_metadata(|metadata: Option<&usize>| {
738                                assert_eq!(*metadata.unwrap(), expected_id);
739                            });
740                        }
741                    }
742                }
743            }
744
745            let mut list = List::new(state.clone());
746            let (size, logical_scroll_top) = list.layout(
747                SizeConstraint::new(vec2f(0., 0.), vec2f(width, height)),
748                &mut presenter.build_layout_context(false, cx),
749            );
750            assert_eq!(size, vec2f(width, height));
751            last_logical_scroll_top = Some(logical_scroll_top);
752
753            let state = state.0.borrow();
754            log::info!("items {:?}", state.items.items(&()));
755
756            let scroll_top = state.scroll_top(&logical_scroll_top);
757            let rendered_top = (scroll_top - overdraw).max(0.);
758            let rendered_bottom = scroll_top + height + overdraw;
759            let mut item_top = 0.;
760
761            log::info!(
762                "rendered top {:?}, rendered bottom {:?}, scroll top {:?}",
763                rendered_top,
764                rendered_bottom,
765                scroll_top,
766            );
767
768            let mut first_rendered_element_top = None;
769            let mut last_rendered_element_bottom = None;
770            assert_eq!(state.items.summary().count, elements.borrow().len());
771            for (ix, item) in state.items.cursor::<(), ()>().enumerate() {
772                match item {
773                    ListItem::Unrendered => {
774                        let item_bottom = item_top;
775                        assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
776                        item_top = item_bottom;
777                    }
778                    ListItem::Removed(height) => {
779                        let (id, expected_height) = elements.borrow()[ix];
780                        assert_eq!(
781                            *height, expected_height,
782                            "element {} height didn't match",
783                            id
784                        );
785                        let item_bottom = item_top + height;
786                        assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
787                        item_top = item_bottom;
788                    }
789                    ListItem::Rendered(element) => {
790                        let (expected_id, expected_height) = elements.borrow()[ix];
791                        element.with_metadata(|metadata: Option<&usize>| {
792                            assert_eq!(*metadata.unwrap(), expected_id);
793                        });
794                        assert_eq!(element.size().y(), expected_height);
795                        let item_bottom = item_top + element.size().y();
796                        first_rendered_element_top.get_or_insert(item_top);
797                        last_rendered_element_bottom = Some(item_bottom);
798                        assert!(item_bottom > rendered_top || item_top < rendered_bottom);
799                        item_top = item_bottom;
800                    }
801                }
802            }
803
804            match orientation {
805                Orientation::Top => {
806                    if let Some(first_rendered_element_top) = first_rendered_element_top {
807                        assert!(first_rendered_element_top <= scroll_top);
808                    }
809                }
810                Orientation::Bottom => {
811                    if let Some(last_rendered_element_bottom) = last_rendered_element_bottom {
812                        assert!(last_rendered_element_bottom >= scroll_top + height);
813                    }
814                }
815            }
816        }
817    }
818
819    struct TestElement {
820        id: usize,
821        size: Vector2F,
822    }
823
824    impl TestElement {
825        fn new(id: usize, height: f32) -> Self {
826            Self {
827                id,
828                size: vec2f(100., height),
829            }
830        }
831    }
832
833    impl Element for TestElement {
834        type LayoutState = ();
835        type PaintState = ();
836
837        fn layout(&mut self, _: SizeConstraint, _: &mut LayoutContext) -> (Vector2F, ()) {
838            (self.size, ())
839        }
840
841        fn paint(&mut self, _: RectF, _: RectF, _: &mut (), _: &mut PaintContext) {
842            todo!()
843        }
844
845        fn dispatch_event(
846            &mut self,
847            _: &Event,
848            _: RectF,
849            _: &mut (),
850            _: &mut (),
851            _: &mut EventContext,
852        ) -> bool {
853            todo!()
854        }
855
856        fn debug(&self, _: RectF, _: &(), _: &(), _: &DebugContext) -> serde_json::Value {
857            self.id.into()
858        }
859
860        fn metadata(&self) -> Option<&dyn std::any::Any> {
861            Some(&self.id)
862        }
863    }
864}