Clean up List::layout

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

gpui/src/elements/list.rs | 214 ++++++++++++++++------------------------
1 file changed, 87 insertions(+), 127 deletions(-)

Detailed changes

gpui/src/elements/list.rs 🔗

@@ -8,7 +8,7 @@ use crate::{
     DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
     SizeConstraint,
 };
-use std::{cell::RefCell, ops::Range, rc::Rc};
+use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
 
 pub struct List {
     state: ListState,
@@ -106,183 +106,127 @@ impl Element for List {
             )
         }
 
-        let overdraw = state.overdraw;
-        let old_rendered_range = state.rendered_range.clone();
         let old_items = state.items.clone();
-        let orientation = state.orientation;
-        let mut stored_scroll_top = state.logical_scroll_top;
         let mut new_items = SumTree::new();
+        let mut rendered_items = VecDeque::new();
+        let mut rendered_height = 0.;
+        let mut scroll_top = state
+            .logical_scroll_top
+            .unwrap_or_else(|| match state.orientation {
+                Orientation::Top => ListOffset {
+                    item_ix: 0,
+                    offset_in_item: 0.,
+                },
+                Orientation::Bottom => ListOffset {
+                    item_ix: state.items.summary().count,
+                    offset_in_item: 0.,
+                },
+            });
 
-        let mut render_item = |ix, old_item: &ListItem| {
-            let element = if let ListItem::Rendered(element) = old_item {
-                element.clone()
-            } else {
-                let mut element = (state.render_item)(ix, cx);
-                element.layout(item_constraint, cx);
-                element.into()
-            };
-            element
-        };
-
-        // Determine the items that need to be rendered in order to cover the visible area plus the
-        // overdraw. When parked at the end of a bottom-oriented list, this requires rendering items
-        // starting from the end of the list until the visible region is full. In other cases, the
-        // stored scroll can be used.
-        let mut new_rendered_start_ix;
-        let mut rendered_items = Vec::new();
-        let mut scroll_top;
-        if let (Orientation::Bottom, None) = (orientation, stored_scroll_top) {
-            let mut rendered_height = 0.;
-            let mut cursor = old_items.cursor::<Count, ()>();
-            cursor.seek(&Count(old_items.summary().count), Bias::Left, &());
+        // Render items after the scroll top, including those in the trailing overdraw.
+        let mut cursor = old_items.cursor::<Count, ()>();
+        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+        for (ix, item) in cursor.by_ref().enumerate() {
+            if rendered_height - scroll_top.offset_in_item >= size.y() + state.overdraw {
+                break;
+            }
 
-            while let Some(item) = cursor.item() {
-                let element = render_item(cursor.seek_start().0, item);
-                rendered_height += element.size().y();
-                rendered_items.push(ListItem::Rendered(element));
-                if rendered_height >= size.y() {
-                    break;
+            let element = state.render_item(scroll_top.item_ix + ix, item, item_constraint, cx);
+            rendered_height += element.size().y();
+            rendered_items.push_back(ListItem::Rendered(element));
+        }
+
+        // Prepare to start walking upward from the item at the scroll top.
+        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+
+        // If the rendered items do not fill the visible region, then adjust
+        // the scroll top upward.
+        if rendered_height - scroll_top.offset_in_item < size.y() {
+            while rendered_height < size.y() {
+                cursor.prev(&());
+                if let Some(item) = cursor.item() {
+                    let element =
+                        state.render_item(cursor.seek_start().0, item, item_constraint, cx);
+                    rendered_height += element.size().y();
+                    rendered_items.push_front(ListItem::Rendered(element));
                 } else {
-                    cursor.prev(&());
+                    break;
                 }
             }
+
             scroll_top = ListOffset {
                 item_ix: cursor.seek_start().0,
                 offset_in_item: rendered_height - size.y(),
             };
 
-            cursor.prev(&());
-            while let Some(item) = cursor.item() {
-                let element = render_item(cursor.seek_start().0, item);
-                rendered_height += element.size().y();
-                rendered_items.push(ListItem::Rendered(element));
-                if rendered_height >= size.y() + overdraw {
-                    break;
-                } else {
-                    cursor.prev(&());
-                }
-            }
-
-            rendered_items.reverse();
-            new_rendered_start_ix = cursor.seek_start().0;
-        } else {
-            scroll_top = stored_scroll_top.unwrap_or_default();
-            let mut rendered_height = 0.;
-            let mut cursor = old_items.cursor::<Count, ()>();
-            cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
-            for (ix, item) in cursor.by_ref().enumerate() {
-                if rendered_height >= scroll_top.offset_in_item + size.y() + overdraw {
-                    break;
+            match state.orientation {
+                Orientation::Top => {
+                    scroll_top.offset_in_item = scroll_top.offset_in_item.max(0.);
+                    state.logical_scroll_top = Some(scroll_top);
                 }
-
-                let element = render_item(scroll_top.item_ix + ix, item);
-                rendered_height += element.size().y();
-                rendered_items.push(ListItem::Rendered(element));
-            }
-            rendered_items.reverse();
-
-            let mut adjust_scroll_top = (rendered_height - scroll_top.offset_in_item) < size.y();
-
-            new_rendered_start_ix = scroll_top.item_ix;
-            cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
-            cursor.prev(&());
-            let mut remaining_overdraw = overdraw - scroll_top.offset_in_item;
-
-            if adjust_scroll_top && rendered_height >= size.y() {
-                let offset_in_item = rendered_height - size.y();
-                scroll_top = ListOffset {
-                    item_ix: new_rendered_start_ix,
-                    offset_in_item,
-                };
-                stored_scroll_top = Some(scroll_top);
-                remaining_overdraw = overdraw - offset_in_item;
-                adjust_scroll_top = false;
-            }
-
-            while let Some(item) = cursor.item() {
-                let element = render_item(cursor.seek_start().0, item);
-                let next_rendered_height = rendered_height + element.size().y();
-
-                if adjust_scroll_top && next_rendered_height >= size.y() {
-                    let offset_in_item = next_rendered_height - size.y();
+                Orientation::Bottom => {
                     scroll_top = ListOffset {
-                        item_ix: new_rendered_start_ix - 1,
-                        offset_in_item,
+                        item_ix: cursor.seek_start().0,
+                        offset_in_item: rendered_height - size.y(),
                     };
-                    stored_scroll_top = Some(scroll_top);
-                    remaining_overdraw = overdraw + (element.size().y() - offset_in_item);
-                    adjust_scroll_top = false;
-                }
-
-                if !adjust_scroll_top && remaining_overdraw <= 0. {
-                    break;
+                    state.logical_scroll_top = None;
                 }
+            };
+        }
 
-                rendered_height = next_rendered_height;
-                remaining_overdraw -= element.size().y();
-                rendered_items.push(ListItem::Rendered(element));
-
-                new_rendered_start_ix -= 1;
-
-                cursor.prev(&());
-            }
-            rendered_items.reverse();
-
-            if rendered_height < size.y() {
-                stored_scroll_top = None;
-                scroll_top = match orientation {
-                    Orientation::Top => Default::default(),
-                    Orientation::Bottom => ListOffset {
-                        item_ix: cursor.seek_start().0,
-                        offset_in_item: rendered_height - size.y(),
-                    },
-                };
+        // Render items in the leading overdraw.
+        let mut leading_overdraw = scroll_top.offset_in_item;
+        while leading_overdraw < state.overdraw {
+            cursor.prev(&());
+            if let Some(item) = cursor.item() {
+                let element = state.render_item(cursor.seek_start().0, item, item_constraint, cx);
+                leading_overdraw += element.size().y();
+                rendered_items.push_front(ListItem::Rendered(element));
+            } else {
+                break;
             }
         }
+
         let new_rendered_range =
-            new_rendered_start_ix..new_rendered_start_ix + rendered_items.len();
+            cursor.seek_start().0..(cursor.seek_start().0 + rendered_items.len());
 
         let mut cursor = old_items.cursor::<Count, ()>();
 
-        // Discard any rendered elements before the new rendered range.
-        if old_rendered_range.start < new_rendered_range.start {
+        if state.rendered_range.start < new_rendered_range.start {
             new_items.push_tree(
-                cursor.slice(&Count(old_rendered_range.start), Bias::Right, &()),
+                cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
                 &(),
             );
-            let remove_to = old_rendered_range.end.min(new_rendered_range.start);
+            let remove_to = state.rendered_range.end.min(new_rendered_range.start);
             while cursor.seek_start().0 < remove_to {
                 new_items.push(cursor.item().unwrap().remove(), &());
                 cursor.next(&());
             }
         }
-
         new_items.push_tree(
             cursor.slice(&Count(new_rendered_range.start), Bias::Right, &()),
             &(),
         );
+
         new_items.extend(rendered_items, &());
         cursor.seek(&Count(new_rendered_range.end), Bias::Right, &());
 
-        if new_rendered_range.end < old_rendered_range.start {
+        if new_rendered_range.end < state.rendered_range.start {
             new_items.push_tree(
-                cursor.slice(&Count(old_rendered_range.start), Bias::Right, &()),
+                cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
                 &(),
             );
         }
-
-        while cursor.seek_start().0 < old_rendered_range.end {
+        while cursor.seek_start().0 < state.rendered_range.end {
             new_items.push(cursor.item().unwrap().remove(), &());
             cursor.next(&());
         }
 
         new_items.push_tree(cursor.suffix(&()), &());
 
-        drop(cursor);
         state.items = new_items;
         state.rendered_range = new_rendered_range;
         state.last_layout_width = Some(size.x());
-        state.logical_scroll_top = stored_scroll_top;
         (size, scroll_top)
     }
 
@@ -429,6 +373,22 @@ impl ListState {
 }
 
 impl StateInner {
+    fn render_item(
+        &mut self,
+        ix: usize,
+        existing_item: &ListItem,
+        constraint: SizeConstraint,
+        cx: &mut LayoutContext,
+    ) -> ElementRc {
+        if let ListItem::Rendered(element) = existing_item {
+            element.clone()
+        } else {
+            let mut element = (self.render_item)(ix, cx);
+            element.layout(constraint, cx);
+            element.into()
+        }
+    }
+
     fn visible_range(&self, height: f32, scroll_top: &ListOffset) -> Range<usize> {
         let mut cursor = self.items.cursor::<Count, Height>();
         cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());