Uncomment `List` tests and fix related problems in `WindowContext`

Antonio Scandurra created

Change summary

crates/gpui/src/app.rs           |  81 -----
crates/gpui/src/app/window.rs    | 102 ++----
crates/gpui/src/elements/list.rs | 512 ++++++++++++++++-----------------
3 files changed, 294 insertions(+), 401 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -45,7 +45,7 @@ use crate::{
     executor::{self, Task},
     keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
     platform::{
-        self, Appearance, FontSystem, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton,
+        self, FontSystem, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton,
         PathPromptOptions, Platform, PromptLevel, WindowBounds, WindowOptions,
     },
     util::post_inc,
@@ -872,60 +872,6 @@ impl AppContext {
         self.active_labeled_tasks.values().cloned()
     }
 
-    pub fn render_view(&mut self, params: RenderParams) -> Result<Box<dyn AnyRootElement>> {
-        todo!()
-        // let window_id = params.window_id;
-        // let view_id = params.view_id;
-        // let mut view = self
-        //     .views
-        //     .remove(&(window_id, view_id))
-        //     .ok_or_else(|| anyhow!("view not found"))?;
-        // let element = view.render(params, self);
-        // self.views.insert((window_id, view_id), view);
-        // Ok(element)
-    }
-
-    pub fn render_views(
-        &mut self,
-        window_id: usize,
-        titlebar_height: f32,
-        appearance: Appearance,
-    ) -> HashMap<usize, Box<dyn AnyRootElement>> {
-        todo!()
-        // self.start_frame();
-        // #[allow(clippy::needless_collect)]
-        // let view_ids = self
-        //     .views
-        //     .keys()
-        //     .filter_map(|(win_id, view_id)| {
-        //         if *win_id == window_id {
-        //             Some(*view_id)
-        //         } else {
-        //             None
-        //         }
-        //     })
-        //     .collect::<Vec<_>>();
-
-        // view_ids
-        //     .into_iter()
-        //     .map(|view_id| {
-        //         (
-        //             view_id,
-        //             self.render_view(RenderParams {
-        //                 window_id,
-        //                 view_id,
-        //                 titlebar_height,
-        //                 hovered_region_ids: Default::default(),
-        //                 clicked_region_ids: None,
-        //                 refreshing: false,
-        //                 appearance,
-        //             })
-        //             .unwrap(),
-        //         )
-        //     })
-        //     .collect()
-    }
-
     pub(crate) fn start_frame(&mut self) {
         self.frame_count += 1;
     }
@@ -2488,19 +2434,8 @@ impl UpdateView for AppContext {
     where
         T: View,
     {
-        self.update_window(handle.window_id, |cx| {
-            cx.update_any_view(handle.view_id, |view, cx| {
-                let mut cx = ViewContext::mutable(cx, handle.view_id);
-                update(
-                    view.as_any_mut()
-                        .downcast_mut()
-                        .expect("downcast is type safe"),
-                    &mut cx,
-                )
-            })
-            .unwrap() // TODO: Are these unwraps safe?
-        })
-        .unwrap()
+        self.update_window(handle.window_id, |cx| cx.update_view(handle, update))
+            .unwrap() // TODO: Is this unwrap safe?
     }
 }
 
@@ -3941,16 +3876,6 @@ impl<'a, T> DerefMut for Reference<'a, T> {
     }
 }
 
-pub struct RenderParams {
-    pub window_id: usize,
-    pub view_id: usize,
-    pub titlebar_height: f32,
-    pub hovered_region_ids: HashSet<MouseRegionId>,
-    pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
-    pub refreshing: bool,
-    pub appearance: Appearance,
-}
-
 #[derive(Debug, Clone, Default)]
 pub struct MouseState {
     pub(crate) hovered: bool,

crates/gpui/src/app/window.rs 🔗

@@ -14,11 +14,11 @@ use crate::{
     text_layout::TextLayoutCache,
     util::post_inc,
     AnyView, AnyViewHandle, AnyWeakViewHandle, AppContext, Drawable, Entity, ModelContext,
-    ModelHandle, MouseRegion, MouseRegionId, ParentId, ReadView, RenderParams, SceneBuilder,
-    UpdateModel, UpdateView, UpgradeViewHandle, View, ViewContext, ViewHandle, WeakViewHandle,
+    ModelHandle, MouseRegion, MouseRegionId, ParentId, ReadView, SceneBuilder, UpdateModel,
+    UpdateView, UpgradeViewHandle, View, ViewContext, ViewHandle, WeakViewHandle,
     WindowInvalidation,
 };
-use anyhow::bail;
+use anyhow::{anyhow, bail, Result};
 use collections::{HashMap, HashSet};
 use pathfinder_geometry::vector::{vec2f, Vector2F};
 use postage::oneshot;
@@ -74,7 +74,7 @@ impl Window {
             invalidation: None,
             is_fullscreen: false,
             platform_window,
-            rendered_views: cx.render_views(window_id, titlebar_height, appearance),
+            rendered_views: Default::default(),
             cursor_regions: Default::default(),
             mouse_regions: Default::default(),
             text_layout_cache: TextLayoutCache::new(cx.font_system.clone()),
@@ -91,6 +91,9 @@ impl Window {
         let root_view = window_context
             .build_and_insert_view(ParentId::Root, |cx| Some(build_view(cx)))
             .unwrap();
+        if let Some(mut invalidation) = window_context.window.invalidation.take() {
+            window_context.invalidate(&mut invalidation, appearance);
+        }
         window.focused_view_id = Some(root_view.id());
         window.root_view = Some(root_view.into_any());
         window
@@ -150,7 +153,16 @@ impl UpdateView for WindowContext<'_, '_> {
     where
         T: View,
     {
-        self.app_context.update_view(handle, update)
+        self.update_any_view(handle.view_id, |view, cx| {
+            let mut cx = ViewContext::mutable(cx, handle.view_id);
+            update(
+                view.as_any_mut()
+                    .downcast_mut()
+                    .expect("downcast is type safe"),
+                &mut cx,
+            )
+        })
+        .unwrap() // TODO: Is this unwrap safe?
     }
 }
 
@@ -678,7 +690,6 @@ impl<'a: 'b, 'b> WindowContext<'a, 'b> {
             self.window.rendered_views.remove(view_id);
         }
         for view_id in &invalidation.updated {
-            let window_id = self.window_id;
             let titlebar_height = self.window.titlebar_height;
             let hovered_region_ids = self.window.hovered_region_ids.clone();
             let clicked_region_ids = self
@@ -688,7 +699,6 @@ impl<'a: 'b, 'b> WindowContext<'a, 'b> {
 
             let element = self
                 .render_view(RenderParams {
-                    window_id,
                     view_id: *view_id,
                     titlebar_height,
                     hovered_region_ids,
@@ -701,39 +711,16 @@ impl<'a: 'b, 'b> WindowContext<'a, 'b> {
         }
     }
 
-    pub fn refresh(&mut self, invalidation: &mut WindowInvalidation, appearance: Appearance) {
-        self.invalidate(invalidation, appearance);
-
-        let view_ids = self
-            .window
-            .rendered_views
-            .keys()
-            .copied()
-            .collect::<Vec<_>>();
-
-        for view_id in view_ids {
-            if !invalidation.updated.contains(&view_id) {
-                let window_id = self.window_id;
-                let titlebar_height = self.window.titlebar_height;
-                let hovered_region_ids = self.window.hovered_region_ids.clone();
-                let clicked_region_ids = self
-                    .window
-                    .clicked_button
-                    .map(|button| (self.window.clicked_region_ids.clone(), button));
-                let element = self
-                    .render_view(RenderParams {
-                        window_id,
-                        view_id,
-                        titlebar_height,
-                        hovered_region_ids,
-                        clicked_region_ids,
-                        refreshing: true,
-                        appearance,
-                    })
-                    .unwrap();
-                self.window.rendered_views.insert(view_id, element);
-            }
-        }
+    pub fn render_view(&mut self, params: RenderParams) -> Result<Box<dyn AnyRootElement>> {
+        let window_id = self.window_id;
+        let view_id = params.view_id;
+        let mut view = self
+            .views
+            .remove(&(window_id, view_id))
+            .ok_or_else(|| anyhow!("view not found"))?;
+        let element = view.render(self, view_id);
+        self.views.insert((window_id, view_id), view);
+        Ok(element)
     }
 
     pub fn build_scene(&mut self) -> Scene {
@@ -854,32 +841,6 @@ impl<'a: 'b, 'b> WindowContext<'a, 'b> {
         self.window.platform_window.prompt(level, msg, answers)
     }
 
-    fn add_view<T, F>(&mut self, parent: &AnyViewHandle, build_view: F) -> ViewHandle<T>
-    where
-        T: View,
-        F: FnOnce(&mut ViewContext<T>) -> T,
-    {
-        if parent.window_id == self.window_id {
-            self.build_and_insert_view(ParentId::View(parent.view_id), |cx| Some(build_view(cx)))
-                .unwrap()
-        } else {
-            self.app_context.add_view(parent, build_view)
-        }
-    }
-
-    fn add_option_view<T, F>(
-        &mut self,
-        parent_handle: impl Into<AnyViewHandle>,
-        build_view: F,
-    ) -> Option<ViewHandle<T>>
-    where
-        T: View,
-        F: FnOnce(&mut ViewContext<T>) -> Option<T>,
-    {
-        let parent_handle = parent_handle.into();
-        self.build_and_insert_view(ParentId::View(parent_handle.view_id), build_view)
-    }
-
     pub fn replace_root_view<V, F>(&mut self, build_root_view: F) -> ViewHandle<V>
     where
         V: View,
@@ -923,6 +884,15 @@ impl<'a: 'b, 'b> WindowContext<'a, 'b> {
     }
 }
 
+pub struct RenderParams {
+    pub view_id: usize,
+    pub titlebar_height: f32,
+    pub hovered_region_ids: HashSet<MouseRegionId>,
+    pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
+    pub refreshing: bool,
+    pub appearance: Appearance,
+}
+
 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
 pub enum Axis {
     #[default]

crates/gpui/src/elements/list.rs 🔗

@@ -648,267 +648,265 @@ mod tests {
     use super::*;
     use crate::{elements::Empty, geometry::vector::vec2f, Entity};
     use rand::prelude::*;
+    use std::env;
 
     #[crate::test(self)]
     fn test_layout(cx: &mut crate::AppContext) {
-        todo!()
-        // let (_, view) = cx.add_window(Default::default(), |_| TestView);
-        // let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
-
-        // let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)]));
-
-        // let state = view.update(cx, |_, cx| {
-        //     ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, cx, {
-        //         let elements = elements.clone();
-        //         move |_, ix, _| {
-        //             let (id, height) = elements.borrow()[ix];
-        //             TestElement::new(id, height).boxed()
-        //         }
-        //     })
-        // });
-
-        // let mut list = List::new(state.clone());
-        // let (size, _) = list.layout(
-        //     constraint,
-        //     &mut presenter.build_layout_context(vec2f(100., 40.), false, cx),
-        // );
-        // assert_eq!(size, vec2f(100., 40.));
-        // assert_eq!(
-        //     state.0.borrow().items.summary().clone(),
-        //     ListItemSummary {
-        //         count: 3,
-        //         rendered_count: 3,
-        //         unrendered_count: 0,
-        //         height: 150.
-        //     }
-        // );
-
-        // state.0.borrow_mut().scroll(
-        //     &ListOffset {
-        //         item_ix: 0,
-        //         offset_in_item: 0.,
-        //     },
-        //     40.,
-        //     vec2f(0., -54.),
-        //     true,
-        //     &mut presenter.build_event_context(&mut Default::default(), cx),
-        // );
-        // let (_, logical_scroll_top) = list.layout(
-        //     constraint,
-        //     &mut presenter.build_layout_context(vec2f(100., 40.), false, cx),
-        // );
-        // assert_eq!(
-        //     logical_scroll_top,
-        //     ListOffset {
-        //         item_ix: 2,
-        //         offset_in_item: 4.
-        //     }
-        // );
-        // assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 54.);
-
-        // elements.borrow_mut().splice(1..2, vec![(3, 40.), (4, 50.)]);
-        // elements.borrow_mut().push((5, 60.));
-        // state.splice(1..2, 2);
-        // state.splice(4..4, 1);
-        // assert_eq!(
-        //     state.0.borrow().items.summary().clone(),
-        //     ListItemSummary {
-        //         count: 5,
-        //         rendered_count: 2,
-        //         unrendered_count: 3,
-        //         height: 120.
-        //     }
-        // );
-
-        // let (size, logical_scroll_top) = list.layout(
-        //     constraint,
-        //     &mut presenter.build_layout_context(vec2f(100., 40.), false, cx),
-        // );
-        // assert_eq!(size, vec2f(100., 40.));
-        // assert_eq!(
-        //     state.0.borrow().items.summary().clone(),
-        //     ListItemSummary {
-        //         count: 5,
-        //         rendered_count: 5,
-        //         unrendered_count: 0,
-        //         height: 270.
-        //     }
-        // );
-        // assert_eq!(
-        //     logical_scroll_top,
-        //     ListOffset {
-        //         item_ix: 3,
-        //         offset_in_item: 4.
-        //     }
-        // );
-        // assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.);
-    }
-
-    #[crate::test(self, iterations = 10, seed = 0)]
+        cx.add_window(Default::default(), |cx| {
+            let mut view = TestView;
+            let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
+            let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)]));
+            let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, {
+                let elements = elements.clone();
+                move |_, ix, _| {
+                    let (id, height) = elements.borrow()[ix];
+                    TestElement::new(id, height).boxed()
+                }
+            });
+
+            let mut list = List::new(state.clone());
+            let (size, _) = list.layout(constraint, &mut view, cx);
+            assert_eq!(size, vec2f(100., 40.));
+            assert_eq!(
+                state.0.borrow().items.summary().clone(),
+                ListItemSummary {
+                    count: 3,
+                    rendered_count: 3,
+                    unrendered_count: 0,
+                    height: 150.
+                }
+            );
+
+            state.0.borrow_mut().scroll(
+                &ListOffset {
+                    item_ix: 0,
+                    offset_in_item: 0.,
+                },
+                40.,
+                vec2f(0., -54.),
+                true,
+                &mut view,
+                cx,
+            );
+
+            let (_, logical_scroll_top) = list.layout(constraint, &mut view, cx);
+            assert_eq!(
+                logical_scroll_top,
+                ListOffset {
+                    item_ix: 2,
+                    offset_in_item: 4.
+                }
+            );
+            assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 54.);
+
+            elements.borrow_mut().splice(1..2, vec![(3, 40.), (4, 50.)]);
+            elements.borrow_mut().push((5, 60.));
+            state.splice(1..2, 2);
+            state.splice(4..4, 1);
+            assert_eq!(
+                state.0.borrow().items.summary().clone(),
+                ListItemSummary {
+                    count: 5,
+                    rendered_count: 2,
+                    unrendered_count: 3,
+                    height: 120.
+                }
+            );
+
+            let (size, logical_scroll_top) = list.layout(constraint, &mut view, cx);
+            assert_eq!(size, vec2f(100., 40.));
+            assert_eq!(
+                state.0.borrow().items.summary().clone(),
+                ListItemSummary {
+                    count: 5,
+                    rendered_count: 5,
+                    unrendered_count: 0,
+                    height: 270.
+                }
+            );
+            assert_eq!(
+                logical_scroll_top,
+                ListOffset {
+                    item_ix: 3,
+                    offset_in_item: 4.
+                }
+            );
+            assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.);
+
+            view
+        });
+    }
+
+    #[crate::test(self, iterations = 10)]
     fn test_random(cx: &mut crate::AppContext, mut rng: StdRng) {
-        todo!()
-        // let operations = env::var("OPERATIONS")
-        //     .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-        //     .unwrap_or(10);
-
-        // let (window_id, view) = cx.add_window(Default::default(), |_| TestView);
-        // let mut next_id = 0;
-        // let elements = Rc::new(RefCell::new(
-        //     (0..rng.gen_range(0..=20))
-        //         .map(|_| {
-        //             let id = next_id;
-        //             next_id += 1;
-        //             (id, rng.gen_range(0..=200) as f32 / 2.0)
-        //         })
-        //         .collect::<Vec<_>>(),
-        // ));
-        // let orientation = *[Orientation::Top, Orientation::Bottom]
-        //     .choose(&mut rng)
-        //     .unwrap();
-        // let overdraw = rng.gen_range(1..=100) as f32;
-
-        // let state = view.update(cx, |_, cx| {
-        //     ListState::new(elements.borrow().len(), orientation, overdraw, cx, {
-        //         let elements = elements.clone();
-        //         move |_, ix, _| {
-        //             let (id, height) = elements.borrow()[ix];
-        //             TestElement::new(id, height).boxed()
-        //         }
-        //     })
-        // });
-
-        // let mut width = rng.gen_range(0..=2000) as f32 / 2.;
-        // let mut height = rng.gen_range(0..=2000) as f32 / 2.;
-        // log::info!("orientation: {:?}", orientation);
-        // log::info!("overdraw: {}", overdraw);
-        // log::info!("elements: {:?}", elements.borrow());
-        // log::info!("size: ({:?}, {:?})", width, height);
-        // log::info!("==================");
-
-        // let mut last_logical_scroll_top = None;
-        // for _ in 0..operations {
-        //     match rng.gen_range(0..=100) {
-        //         0..=29 if last_logical_scroll_top.is_some() => {
-        //             let delta = vec2f(0., rng.gen_range(-overdraw..=overdraw));
-        //             log::info!(
-        //                 "Scrolling by {:?}, previous scroll top: {:?}",
-        //                 delta,
-        //                 last_logical_scroll_top.unwrap()
-        //             );
-        //             state.0.borrow_mut().scroll(
-        //                 last_logical_scroll_top.as_ref().unwrap(),
-        //                 height,
-        //                 delta,
-        //                 true,
-        //                 &mut presenter.build_event_context(&mut Default::default(), cx),
-        //             );
-        //         }
-        //         30..=34 => {
-        //             width = rng.gen_range(0..=2000) as f32 / 2.;
-        //             log::info!("changing width: {:?}", width);
-        //         }
-        //         35..=54 => {
-        //             height = rng.gen_range(0..=1000) as f32 / 2.;
-        //             log::info!("changing height: {:?}", height);
-        //         }
-        //         _ => {
-        //             let mut elements = elements.borrow_mut();
-        //             let end_ix = rng.gen_range(0..=elements.len());
-        //             let start_ix = rng.gen_range(0..=end_ix);
-        //             let new_elements = (0..rng.gen_range(0..10))
-        //                 .map(|_| {
-        //                     let id = next_id;
-        //                     next_id += 1;
-        //                     (id, rng.gen_range(0..=200) as f32 / 2.)
-        //                 })
-        //                 .collect::<Vec<_>>();
-        //             log::info!("splice({:?}, {:?})", start_ix..end_ix, new_elements);
-        //             state.splice(start_ix..end_ix, new_elements.len());
-        //             elements.splice(start_ix..end_ix, new_elements);
-        //             for (ix, item) in state.0.borrow().items.cursor::<()>().enumerate() {
-        //                 if let ListItem::Rendered(element) = item {
-        //                     let (expected_id, _) = elements[ix];
-        //                     element.with_metadata(|metadata: Option<&usize>| {
-        //                         assert_eq!(*metadata.unwrap(), expected_id);
-        //                     });
-        //                 }
-        //             }
-        //         }
-        //     }
-
-        //     let mut list = List::new(state.clone());
-        //     let window_size = vec2f(width, height);
-        //     let (size, logical_scroll_top) = list.layout(
-        //         SizeConstraint::new(vec2f(0., 0.), window_size),
-        //         &mut presenter.build_layout_context(window_size, false, cx),
-        //     );
-        //     assert_eq!(size, window_size);
-        //     last_logical_scroll_top = Some(logical_scroll_top);
-
-        //     let state = state.0.borrow();
-        //     log::info!("items {:?}", state.items.items(&()));
-
-        //     let scroll_top = state.scroll_top(&logical_scroll_top);
-        //     let rendered_top = (scroll_top - overdraw).max(0.);
-        //     let rendered_bottom = scroll_top + height + overdraw;
-        //     let mut item_top = 0.;
-
-        //     log::info!(
-        //         "rendered top {:?}, rendered bottom {:?}, scroll top {:?}",
-        //         rendered_top,
-        //         rendered_bottom,
-        //         scroll_top,
-        //     );
-
-        //     let mut first_rendered_element_top = None;
-        //     let mut last_rendered_element_bottom = None;
-        //     assert_eq!(state.items.summary().count, elements.borrow().len());
-        //     for (ix, item) in state.items.cursor::<()>().enumerate() {
-        //         match item {
-        //             ListItem::Unrendered => {
-        //                 let item_bottom = item_top;
-        //                 assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
-        //                 item_top = item_bottom;
-        //             }
-        //             ListItem::Removed(height) => {
-        //                 let (id, expected_height) = elements.borrow()[ix];
-        //                 assert_eq!(
-        //                     *height, expected_height,
-        //                     "element {} height didn't match",
-        //                     id
-        //                 );
-        //                 let item_bottom = item_top + height;
-        //                 assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
-        //                 item_top = item_bottom;
-        //             }
-        //             ListItem::Rendered(element) => {
-        //                 let (expected_id, expected_height) = elements.borrow()[ix];
-        //                 element.with_metadata(|metadata: Option<&usize>| {
-        //                     assert_eq!(*metadata.unwrap(), expected_id);
-        //                 });
-        //                 assert_eq!(element.size().y(), expected_height);
-        //                 let item_bottom = item_top + element.size().y();
-        //                 first_rendered_element_top.get_or_insert(item_top);
-        //                 last_rendered_element_bottom = Some(item_bottom);
-        //                 assert!(item_bottom > rendered_top || item_top < rendered_bottom);
-        //                 item_top = item_bottom;
-        //             }
-        //         }
-        //     }
-
-        //     match orientation {
-        //         Orientation::Top => {
-        //             if let Some(first_rendered_element_top) = first_rendered_element_top {
-        //                 assert!(first_rendered_element_top <= scroll_top);
-        //             }
-        //         }
-        //         Orientation::Bottom => {
-        //             if let Some(last_rendered_element_bottom) = last_rendered_element_bottom {
-        //                 assert!(last_rendered_element_bottom >= scroll_top + height);
-        //             }
-        //         }
-        //     }
-        // }
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        cx.add_window(Default::default(), |cx| {
+            let mut view = TestView;
+
+            let mut next_id = 0;
+            let elements = Rc::new(RefCell::new(
+                (0..rng.gen_range(0..=20))
+                    .map(|_| {
+                        let id = next_id;
+                        next_id += 1;
+                        (id, rng.gen_range(0..=200) as f32 / 2.0)
+                    })
+                    .collect::<Vec<_>>(),
+            ));
+            let orientation = *[Orientation::Top, Orientation::Bottom]
+                .choose(&mut rng)
+                .unwrap();
+            let overdraw = rng.gen_range(1..=100) as f32;
+
+            let state = ListState::new(elements.borrow().len(), orientation, overdraw, {
+                let elements = elements.clone();
+                move |_, ix, _| {
+                    let (id, height) = elements.borrow()[ix];
+                    TestElement::new(id, height).boxed()
+                }
+            });
+
+            let mut width = rng.gen_range(0..=2000) as f32 / 2.;
+            let mut height = rng.gen_range(0..=2000) as f32 / 2.;
+            log::info!("orientation: {:?}", orientation);
+            log::info!("overdraw: {}", overdraw);
+            log::info!("elements: {:?}", elements.borrow());
+            log::info!("size: ({:?}, {:?})", width, height);
+            log::info!("==================");
+
+            let mut last_logical_scroll_top = None;
+            for _ in 0..operations {
+                match rng.gen_range(0..=100) {
+                    0..=29 if last_logical_scroll_top.is_some() => {
+                        let delta = vec2f(0., rng.gen_range(-overdraw..=overdraw));
+                        log::info!(
+                            "Scrolling by {:?}, previous scroll top: {:?}",
+                            delta,
+                            last_logical_scroll_top.unwrap()
+                        );
+                        state.0.borrow_mut().scroll(
+                            last_logical_scroll_top.as_ref().unwrap(),
+                            height,
+                            delta,
+                            true,
+                            &mut view,
+                            cx,
+                        );
+                    }
+                    30..=34 => {
+                        width = rng.gen_range(0..=2000) as f32 / 2.;
+                        log::info!("changing width: {:?}", width);
+                    }
+                    35..=54 => {
+                        height = rng.gen_range(0..=1000) as f32 / 2.;
+                        log::info!("changing height: {:?}", height);
+                    }
+                    _ => {
+                        let mut elements = elements.borrow_mut();
+                        let end_ix = rng.gen_range(0..=elements.len());
+                        let start_ix = rng.gen_range(0..=end_ix);
+                        let new_elements = (0..rng.gen_range(0..10))
+                            .map(|_| {
+                                let id = next_id;
+                                next_id += 1;
+                                (id, rng.gen_range(0..=200) as f32 / 2.)
+                            })
+                            .collect::<Vec<_>>();
+                        log::info!("splice({:?}, {:?})", start_ix..end_ix, new_elements);
+                        state.splice(start_ix..end_ix, new_elements.len());
+                        elements.splice(start_ix..end_ix, new_elements);
+                        for (ix, item) in state.0.borrow().items.cursor::<()>().enumerate() {
+                            if let ListItem::Rendered(element) = item {
+                                let (expected_id, _) = elements[ix];
+                                element.borrow().with_metadata(|metadata: Option<&usize>| {
+                                    assert_eq!(*metadata.unwrap(), expected_id);
+                                });
+                            }
+                        }
+                    }
+                }
+
+                let mut list = List::new(state.clone());
+                let window_size = vec2f(width, height);
+                let (size, logical_scroll_top) = list.layout(
+                    SizeConstraint::new(vec2f(0., 0.), window_size),
+                    &mut view,
+                    cx,
+                );
+                assert_eq!(size, window_size);
+                last_logical_scroll_top = Some(logical_scroll_top);
+
+                let state = state.0.borrow();
+                log::info!("items {:?}", state.items.items(&()));
+
+                let scroll_top = state.scroll_top(&logical_scroll_top);
+                let rendered_top = (scroll_top - overdraw).max(0.);
+                let rendered_bottom = scroll_top + height + overdraw;
+                let mut item_top = 0.;
+
+                log::info!(
+                    "rendered top {:?}, rendered bottom {:?}, scroll top {:?}",
+                    rendered_top,
+                    rendered_bottom,
+                    scroll_top,
+                );
+
+                let mut first_rendered_element_top = None;
+                let mut last_rendered_element_bottom = None;
+                assert_eq!(state.items.summary().count, elements.borrow().len());
+                for (ix, item) in state.items.cursor::<()>().enumerate() {
+                    match item {
+                        ListItem::Unrendered => {
+                            let item_bottom = item_top;
+                            assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
+                            item_top = item_bottom;
+                        }
+                        ListItem::Removed(height) => {
+                            let (id, expected_height) = elements.borrow()[ix];
+                            assert_eq!(
+                                *height, expected_height,
+                                "element {} height didn't match",
+                                id
+                            );
+                            let item_bottom = item_top + height;
+                            assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
+                            item_top = item_bottom;
+                        }
+                        ListItem::Rendered(element) => {
+                            let (expected_id, expected_height) = elements.borrow()[ix];
+                            let element = element.borrow();
+                            element.with_metadata(|metadata: Option<&usize>| {
+                                assert_eq!(*metadata.unwrap(), expected_id);
+                            });
+                            assert_eq!(element.size().y(), expected_height);
+                            let item_bottom = item_top + element.size().y();
+                            first_rendered_element_top.get_or_insert(item_top);
+                            last_rendered_element_bottom = Some(item_bottom);
+                            assert!(item_bottom > rendered_top || item_top < rendered_bottom);
+                            item_top = item_bottom;
+                        }
+                    }
+                }
+
+                match orientation {
+                    Orientation::Top => {
+                        if let Some(first_rendered_element_top) = first_rendered_element_top {
+                            assert!(first_rendered_element_top <= scroll_top);
+                        }
+                    }
+                    Orientation::Bottom => {
+                        if let Some(last_rendered_element_bottom) = last_rendered_element_bottom {
+                            assert!(last_rendered_element_bottom >= scroll_top + height);
+                        }
+                    }
+                }
+            }
+
+            view
+        });
     }
 
     struct TestView;