Start work on partial rendering for List element

Max Brunsfeld , Nathan Sobo , and Antonio Scandurra created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
Co-Authored-By: Antonio Scandurra <me@as-cii.com>

Change summary

gpui/src/elements/list.rs   | 599 +++++++++++++++++++++++---------------
gpui/src/presenter.rs       |  29 +
gpui/src/sum_tree.rs        |  11 
gpui/src/sum_tree/cursor.rs |  38 ++
zed/src/channel.rs          |   6 
zed/src/chat_panel.rs       |  45 +-
6 files changed, 468 insertions(+), 260 deletions(-)

Detailed changes

gpui/src/elements/list.rs 🔗

@@ -6,7 +6,7 @@ use crate::{
     json::json,
     sum_tree::{self, Bias, SumTree},
     DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
-    RenderContext, SizeConstraint, View,
+    SizeConstraint,
 };
 use std::{cell::RefCell, ops::Range, rc::Rc};
 
@@ -17,7 +17,7 @@ pub struct List {
 #[derive(Clone)]
 pub struct ListState(Rc<RefCell<StateInner>>);
 
-#[derive(Eq, PartialEq)]
+#[derive(Clone, Copy, Eq, PartialEq)]
 pub enum Orientation {
     Top,
     Bottom,
@@ -25,87 +25,56 @@ pub enum Orientation {
 
 struct StateInner {
     last_layout_width: Option<f32>,
-    elements: Vec<Option<ElementRc>>,
-    heights: SumTree<ElementHeight>,
-    scroll_top: Option<(usize, f32)>,
+    render_item: Box<dyn FnMut(usize, &mut LayoutContext) -> ElementBox>,
+    rendered_range: Range<usize>,
+    items: SumTree<ListItem>,
+    scroll_top: Option<ScrollTop>,
     orientation: Orientation,
+    overdraw: usize,
     scroll_handler: Option<Box<dyn FnMut(Range<usize>, &mut EventContext)>>,
 }
 
-#[derive(Clone, Debug)]
-enum ElementHeight {
-    Pending,
-    Ready(f32),
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct ScrollTop {
+    item_ix: usize,
+    offset_in_item: f32,
+}
+
+#[derive(Clone)]
+enum ListItem {
+    Unrendered,
+    Rendered(ElementRc),
+    Removed(f32),
 }
 
 #[derive(Clone, Debug, Default, PartialEq)]
-struct ElementHeightSummary {
+struct ListItemSummary {
     count: usize,
-    pending_count: usize,
+    rendered_count: usize,
+    unrendered_count: usize,
     height: f32,
 }
 
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
 struct Count(usize);
 
-#[derive(Clone, Debug, Default)]
-struct PendingCount(usize);
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct RenderedCount(usize);
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct UnrenderedCount(usize);
 
 #[derive(Clone, Debug, Default)]
 struct Height(f32);
 
 impl List {
-    pub fn new<F, I, V>(state: ListState, cx: &RenderContext<V>, build_items: F) -> Self
-    where
-        F: Fn(Range<usize>) -> I,
-        I: IntoIterator<Item = ElementBox>,
-        V: View,
-    {
-        {
-            let state = &mut *state.0.borrow_mut();
-            if cx.refreshing {
-                let elements = (build_items)(0..state.elements.len());
-                state.last_layout_width = None;
-                state.elements.clear();
-                state
-                    .elements
-                    .extend(elements.into_iter().map(|e| Some(e.into())));
-                state.heights = SumTree::new();
-                state.heights.extend(
-                    (0..state.elements.len()).map(|_| ElementHeight::Pending),
-                    &(),
-                );
-            } else {
-                let mut cursor = state.heights.cursor::<PendingCount, Count>();
-                cursor.seek(&PendingCount(1), sum_tree::Bias::Left, &());
-
-                while cursor.item().is_some() {
-                    let start_ix = cursor.sum_start().0;
-                    while cursor.item().map_or(false, |h| h.is_pending()) {
-                        cursor.next(&());
-                    }
-                    let end_ix = cursor.sum_start().0;
-                    if end_ix > start_ix {
-                        state.elements.splice(
-                            start_ix..end_ix,
-                            (build_items)(start_ix..end_ix)
-                                .into_iter()
-                                .map(|e| Some(e.into())),
-                        );
-                    }
-
-                    cursor.seek(&PendingCount(cursor.seek_start().0 + 1), Bias::Left, &());
-                }
-            }
-        }
-
+    pub fn new(state: ListState) -> Self {
         Self { state }
     }
 }
 
 impl Element for List {
-    type LayoutState = Vec<ElementRc>;
-
+    type LayoutState = ScrollTop;
     type PaintState = ();
 
     fn layout(
@@ -114,79 +83,162 @@ impl Element for List {
         cx: &mut LayoutContext,
     ) -> (Vector2F, Self::LayoutState) {
         let state = &mut *self.state.0.borrow_mut();
+        let size = constraint.max;
         let mut item_constraint = constraint;
         item_constraint.min.set_y(0.);
         item_constraint.max.set_y(f32::INFINITY);
 
-        if state.last_layout_width == Some(constraint.max.x()) {
-            let mut old_heights = state.heights.cursor::<PendingCount, ElementHeightSummary>();
-            let mut new_heights = old_heights.slice(&PendingCount(1), sum_tree::Bias::Left, &());
-
-            while let Some(height) = old_heights.item() {
-                if height.is_pending() {
-                    let element = &mut state.elements[old_heights.sum_start().count];
-                    let element_size = element.as_mut().unwrap().layout(item_constraint, cx);
-                    new_heights.push(ElementHeight::Ready(element_size.y()), &());
-                    old_heights.next(&());
-                } else {
-                    new_heights.push_tree(
-                        old_heights.slice(
-                            &PendingCount(old_heights.sum_start().pending_count + 1),
-                            Bias::Left,
-                            &(),
-                        ),
-                        &(),
-                    );
+        if state.last_layout_width != Some(constraint.max.x()) {
+            state.rendered_range = 0..0;
+            state.items = SumTree::from_iter(
+                (0..state.items.summary().count).map(|_| ListItem::Unrendered),
+                &(),
+            )
+        }
+
+        let overdraw = state.overdraw;
+        let old_rendered_range = state.rendered_range.clone();
+        let old_items = state.items.clone();
+        let orientation = state.orientation;
+        let stored_scroll_top = state.scroll_top;
+        let mut new_items = SumTree::new();
+
+        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 scroll top. 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 scroll_top;
+        let trailing_items;
+        if let (Orientation::Bottom, None) = (orientation, stored_scroll_top) {
+            let mut rendered_height = 0.;
+            let mut cursor = old_items.cursor::<Count, ()>();
+
+            let mut visible_items = Vec::new();
+            cursor.seek(&Count(old_items.summary().count), Bias::Left, &());
+            while let Some(item) = cursor.item() {
+                if rendered_height >= size.y() {
+                    break;
                 }
+
+                let element = render_item(cursor.seek_start().0, item);
+                rendered_height += element.size().y();
+                visible_items.push(ListItem::Rendered(element));
+                cursor.prev(&());
             }
 
-            drop(old_heights);
-            state.heights = new_heights;
+            scroll_top = ScrollTop {
+                item_ix: cursor.seek_start().0,
+                offset_in_item: rendered_height - size.y(),
+            };
+            visible_items.reverse();
+            trailing_items = Some(visible_items);
         } else {
-            state.heights = SumTree::new();
-            for element in &mut state.elements {
-                let element = element.as_mut().unwrap();
-                let size = element.layout(item_constraint, cx);
-                state.heights.push(ElementHeight::Ready(size.y()), &());
+            scroll_top = stored_scroll_top.unwrap_or_default();
+            trailing_items = None;
+        }
+
+        let new_rendered_range_start = scroll_top.item_ix.saturating_sub(overdraw);
+        let mut cursor = old_items.cursor::<Count, ()>();
+
+        // Discard any rendered elements before the overdraw window.
+        if old_rendered_range.start < new_rendered_range_start {
+            new_items.push_tree(
+                cursor.slice(&Count(old_rendered_range.start), Bias::Right, &()),
+                &(),
+            );
+            let remove_to = old_rendered_range.end.min(new_rendered_range_start);
+            while cursor.seek_start().0 < remove_to {
+                new_items.push(cursor.item().unwrap().remove(), &());
+                cursor.next(&());
             }
-            state.last_layout_width = Some(constraint.max.x());
         }
 
-        let size = constraint.max;
-        let visible_elements = state.elements[state.visible_range(size.y())]
-            .iter()
-            .map(|e| e.clone().unwrap())
-            .collect();
-        (size, visible_elements)
-    }
+        new_items.push_tree(
+            cursor.slice(&Count(new_rendered_range_start), Bias::Right, &()),
+            &(),
+        );
 
-    fn paint(
-        &mut self,
-        bounds: RectF,
-        visible_elements: &mut Self::LayoutState,
-        cx: &mut PaintContext,
-    ) {
-        cx.scene.push_layer(Some(bounds));
-        let state = &mut *self.state.0.borrow_mut();
-        let visible_range = state.visible_range(bounds.height());
+        // Ensure that all items in the overdraw window before the visible range are rendered.
+        while cursor.seek_start().0 < scroll_top.item_ix {
+            new_items.push(
+                ListItem::Rendered(render_item(cursor.seek_start().0, cursor.item().unwrap())),
+                &(),
+            );
+            cursor.next(&());
+        }
 
-        let mut item_top = {
-            let mut cursor = state.heights.cursor::<Count, Height>();
-            cursor.seek(&Count(visible_range.start), Bias::Right, &());
-            cursor.sum_start().0
-        };
-        if state.orientation == Orientation::Bottom
-            && bounds.height() > state.heights.summary().height
-        {
-            item_top += bounds.height() - state.heights.summary().height;
+        // The remaining items may have already been rendered, when parked at the
+        // end of a bottom-oriented list. If so, append them.
+        let new_rendered_range_end;
+        if let Some(trailing_items) = trailing_items {
+            new_rendered_range_end = new_rendered_range_start + trailing_items.len();
+            new_items.extend(trailing_items, &());
+        } else {
+            // Ensure that enough items are rendered to fill the visible range.
+            let mut rendered_top = -scroll_top.offset_in_item;
+            while let Some(item) = cursor.item() {
+                if rendered_top >= size.y() {
+                    break;
+                }
+
+                let element = render_item(cursor.seek_start().0, item);
+                rendered_top += element.size().y();
+                new_items.push(ListItem::Rendered(element), &());
+                cursor.next(&());
+            }
+
+            // Ensure that all items in the overdraw window after the visible range
+            // are rendered.
+            new_rendered_range_end =
+                (cursor.seek_start().0 + overdraw).min(old_items.summary().count);
+            while cursor.seek_start().0 < new_rendered_range_end {
+                new_items.push(
+                    ListItem::Rendered(render_item(cursor.seek_start().0, cursor.item().unwrap())),
+                    &(),
+                );
+                cursor.next(&());
+            }
+
+            // Preserve the remainder of the items, but discard any rendered items after
+            // the overdraw window.
+            if cursor.seek_start().0 < old_rendered_range.start {
+                new_items.push_tree(
+                    cursor.slice(&Count(old_rendered_range.start), Bias::Right, &()),
+                    &(),
+                );
+            }
+            while cursor.seek_start().0 < old_rendered_range.end {
+                new_items.push(cursor.item().unwrap().remove(), &());
+                cursor.next(&());
+            }
+            new_items.push_tree(cursor.suffix(&()), &());
         }
-        let scroll_top = state.scroll_top(bounds.height());
 
-        for element in visible_elements {
-            let origin = bounds.origin() + vec2f(0., item_top - scroll_top);
+        drop(cursor);
+        state.items = new_items;
+        state.rendered_range = new_rendered_range_start..new_rendered_range_end;
+        (constraint.max, scroll_top)
+    }
+
+    fn paint(&mut self, bounds: RectF, scroll_top: &mut ScrollTop, cx: &mut PaintContext) {
+        cx.scene.push_layer(Some(bounds));
+
+        let state = &mut *self.state.0.borrow_mut();
+        for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
             element.paint(origin, cx);
-            item_top += element.size().y();
         }
+
         cx.scene.pop_layer();
     }
 
@@ -194,14 +246,14 @@ impl Element for List {
         &mut self,
         event: &Event,
         bounds: RectF,
-        visible_elements: &mut Self::LayoutState,
+        scroll_top: &mut ScrollTop,
         _: &mut (),
         cx: &mut EventContext,
     ) -> bool {
         let mut handled = false;
 
         let mut state = self.state.0.borrow_mut();
-        for element in visible_elements {
+        for (mut element, _) in state.visible_elements(bounds, scroll_top) {
             handled = element.dispatch_event(event, cx) || handled;
         }
 
@@ -212,7 +264,7 @@ impl Element for List {
                 precise,
             } => {
                 if bounds.contains_point(*position) {
-                    if state.scroll(*position, *delta, *precise, bounds.height(), cx) {
+                    if state.scroll(scroll_top, bounds.height(), *delta, *precise, cx) {
                         handled = true;
                     }
                 }
@@ -226,61 +278,91 @@ impl Element for List {
     fn debug(
         &self,
         bounds: RectF,
-        visible_elements: &Self::LayoutState,
+        scroll_top: &Self::LayoutState,
         _: &(),
         cx: &DebugContext,
     ) -> serde_json::Value {
         let state = self.state.0.borrow_mut();
-        let visible_range = state.visible_range(bounds.height());
-        let visible_elements = visible_elements
-            .iter()
-            .map(|e| e.debug(cx))
+        let visible_elements = state
+            .visible_elements(bounds, scroll_top)
+            .map(|e| e.0.debug(cx))
             .collect::<Vec<_>>();
+        let visible_range = scroll_top.item_ix..(scroll_top.item_ix + visible_elements.len());
         json!({
             "visible_range": visible_range,
             "visible_elements": visible_elements,
-            "scroll_top": state.scroll_top,
+            "scroll_top": state.scroll_top.map(|top| (top.item_ix, top.offset_in_item)),
         })
     }
 }
 
 impl ListState {
-    pub fn new(element_count: usize, orientation: Orientation) -> Self {
-        let mut heights = SumTree::new();
-        heights.extend((0..element_count).map(|_| ElementHeight::Pending), &());
+    pub fn new<F>(
+        element_count: usize,
+        orientation: Orientation,
+        min_overdraw: usize,
+        render_item: F,
+    ) -> Self
+    where
+        F: 'static + FnMut(usize, &mut LayoutContext) -> ElementBox,
+    {
+        let mut items = SumTree::new();
+        items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
         Self(Rc::new(RefCell::new(StateInner {
             last_layout_width: None,
-            elements: (0..element_count).map(|_| None).collect(),
-            heights,
+            render_item: Box::new(render_item),
+            rendered_range: 0..0,
+            items,
             scroll_top: None,
             orientation,
+            overdraw: min_overdraw,
             scroll_handler: None,
         })))
     }
 
+    pub fn reset(&self, element_count: usize) {
+        let state = &mut *self.0.borrow_mut();
+        state.scroll_top = None;
+        state.items = SumTree::new();
+        state
+            .items
+            .extend((0..element_count).map(|_| ListItem::Unrendered), &());
+    }
+
     pub fn splice(&self, old_range: Range<usize>, count: usize) {
         let state = &mut *self.0.borrow_mut();
 
-        if let Some((ix, offset)) = state.scroll_top.as_mut() {
-            if old_range.contains(ix) {
-                *ix = old_range.start;
-                *offset = 0.;
-            } else if old_range.end <= *ix {
-                *ix = *ix - (old_range.end - old_range.start) + count;
+        if let Some(ScrollTop {
+            item_ix,
+            offset_in_item,
+        }) = state.scroll_top.as_mut()
+        {
+            if old_range.contains(item_ix) {
+                *item_ix = old_range.start;
+                *offset_in_item = 0.;
+            } else if old_range.end <= *item_ix {
+                *item_ix = *item_ix - (old_range.end - old_range.start) + count;
             }
         }
 
-        let mut old_heights = state.heights.cursor::<Count, ()>();
+        let new_end = old_range.start + count;
+        if old_range.start < state.rendered_range.start {
+            state.rendered_range.start =
+                new_end + state.rendered_range.start.saturating_sub(old_range.end);
+        }
+        if old_range.start < state.rendered_range.end {
+            state.rendered_range.end =
+                new_end + state.rendered_range.end.saturating_sub(old_range.end);
+        }
+
+        let mut old_heights = state.items.cursor::<Count, ()>();
         let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
         old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
 
-        let old_elements = state.elements.splice(old_range, (0..count).map(|_| None));
-        drop(old_elements);
-
-        new_heights.extend((0..count).map(|_| ElementHeight::Pending), &());
+        new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
         new_heights.push_tree(old_heights.suffix(&()), &());
         drop(old_heights);
-        state.heights = new_heights;
+        state.items = new_heights;
     }
 
     pub fn set_scroll_handler(
@@ -292,55 +374,76 @@ impl ListState {
 }
 
 impl StateInner {
-    fn visible_range(&self, height: f32) -> Range<usize> {
-        let mut cursor = self.heights.cursor::<Height, Count>();
-        cursor.seek(&Height(self.scroll_top(height)), Bias::Right, &());
-        let start_ix = cursor.sum_start().0;
-        cursor.seek(&Height(self.scroll_top(height) + height), Bias::Left, &());
-        let end_ix = cursor.sum_start().0;
-        start_ix..self.elements.len().min(end_ix + 1)
+    fn visible_range(&self, height: f32, scroll_top: &ScrollTop) -> Range<usize> {
+        let mut cursor = self.items.cursor::<Count, Height>();
+        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+        let start_y = cursor.sum_start().0 + scroll_top.offset_in_item;
+        let mut cursor = cursor.swap_dimensions();
+        cursor.seek_forward(&Height(start_y + height), Bias::Right, &());
+        scroll_top.item_ix..cursor.sum_start().0
+    }
+
+    fn visible_elements<'a>(
+        &'a self,
+        bounds: RectF,
+        scroll_top: &ScrollTop,
+    ) -> impl Iterator<Item = (ElementRc, Vector2F)> + 'a {
+        let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
+        let mut cursor = self.items.cursor::<Count, ()>();
+        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+        std::iter::from_fn(move || {
+            while let Some(item) = cursor.item() {
+                if item_origin.y() > bounds.max_y() {
+                    break;
+                }
+
+                if let ListItem::Rendered(element) = item {
+                    let result = (element.clone(), item_origin);
+                    item_origin.set_y(item_origin.y() + element.size().y());
+                    cursor.next(&());
+                    return Some(result);
+                }
+
+                cursor.next(&());
+            }
+
+            None
+        })
     }
 
     fn scroll(
         &mut self,
-        _: Vector2F,
+        scroll_top: &ScrollTop,
+        height: f32,
         mut delta: Vector2F,
         precise: bool,
-        height: f32,
         cx: &mut EventContext,
     ) -> bool {
         if !precise {
             delta *= 20.;
         }
 
-        let delta_y;
-        let seek_bias;
-        match self.orientation {
-            Orientation::Top => {
-                delta_y = delta.y();
-                seek_bias = Bias::Right;
-            }
-            Orientation::Bottom => {
-                delta_y = -delta.y();
-                seek_bias = Bias::Left;
-            }
-        };
+        let scroll_max = (self.items.summary().height - height).max(0.);
+        let new_scroll_top = (self.scroll_top(height) + delta.y())
+            .max(0.)
+            .min(scroll_max);
 
-        let scroll_max = (self.heights.summary().height - height).max(0.);
-        let new_scroll_top = (self.scroll_top(height) + delta_y).max(0.).min(scroll_max);
         if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max {
             self.scroll_top = None;
         } else {
-            let mut cursor = self.heights.cursor::<Height, Count>();
-            cursor.seek(&Height(new_scroll_top), seek_bias, &());
-            let ix = cursor.sum_start().0;
-            let offset = new_scroll_top - cursor.seek_start().0;
-            self.scroll_top = Some((ix, offset));
+            let mut cursor = self.items.cursor::<Height, Count>();
+            cursor.seek(&Height(new_scroll_top), Bias::Right, &());
+            let item_ix = cursor.sum_start().0;
+            let offset_in_item = new_scroll_top - cursor.seek_start().0;
+            self.scroll_top = Some(ScrollTop {
+                item_ix,
+                offset_in_item,
+            });
         }
 
         if self.scroll_handler.is_some() {
-            let range = self.visible_range(height);
-            self.scroll_handler.as_mut().unwrap()(range, cx);
+            let visible_range = self.visible_range(height, scroll_top);
+            self.scroll_handler.as_mut().unwrap()(visible_range, cx);
         }
         cx.notify();
 
@@ -348,11 +451,15 @@ impl StateInner {
     }
 
     fn scroll_top(&self, height: f32) -> f32 {
-        let scroll_max = (self.heights.summary().height - height).max(0.);
-        if let Some((ix, offset)) = self.scroll_top {
-            let mut cursor = self.heights.cursor::<Count, Height>();
-            cursor.seek(&Count(ix), Bias::Right, &());
-            (cursor.sum_start().0 + offset).min(scroll_max)
+        let scroll_max = (self.items.summary().height - height).max(0.);
+        if let Some(ScrollTop {
+            item_ix,
+            offset_in_item,
+        }) = self.scroll_top
+        {
+            let mut cursor = self.items.cursor::<Count, Height>();
+            cursor.seek(&Count(item_ix), Bias::Right, &());
+            (cursor.sum_start().0 + offset_in_item).min(scroll_max)
         } else {
             match self.orientation {
                 Orientation::Top => 0.,
@@ -362,78 +469,85 @@ impl StateInner {
     }
 }
 
-impl ElementHeight {
-    fn is_pending(&self) -> bool {
-        matches!(self, ElementHeight::Pending)
+impl ListItem {
+    fn remove(&self) -> Self {
+        match self {
+            ListItem::Unrendered => ListItem::Unrendered,
+            ListItem::Rendered(element) => ListItem::Removed(element.size().y()),
+            ListItem::Removed(height) => ListItem::Removed(*height),
+        }
     }
 }
 
-impl sum_tree::Item for ElementHeight {
-    type Summary = ElementHeightSummary;
+impl sum_tree::Item for ListItem {
+    type Summary = ListItemSummary;
 
     fn summary(&self) -> Self::Summary {
         match self {
-            ElementHeight::Pending => ElementHeightSummary {
+            ListItem::Unrendered => ListItemSummary {
                 count: 1,
-                pending_count: 1,
+                rendered_count: 0,
+                unrendered_count: 1,
                 height: 0.,
             },
-            ElementHeight::Ready(height) => ElementHeightSummary {
+            ListItem::Rendered(element) => ListItemSummary {
                 count: 1,
-                pending_count: 0,
+                rendered_count: 1,
+                unrendered_count: 0,
+                height: element.size().y(),
+            },
+            ListItem::Removed(height) => ListItemSummary {
+                count: 1,
+                rendered_count: 0,
+                unrendered_count: 1,
                 height: *height,
             },
         }
     }
 }
 
-impl sum_tree::Summary for ElementHeightSummary {
+impl sum_tree::Summary for ListItemSummary {
     type Context = ();
 
     fn add_summary(&mut self, summary: &Self, _: &()) {
         self.count += summary.count;
-        self.pending_count += summary.pending_count;
+        self.rendered_count += summary.rendered_count;
+        self.unrendered_count += summary.unrendered_count;
         self.height += summary.height;
     }
 }
 
-impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for ElementHeightSummary {
-    fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) {
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for ListItemSummary {
+    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
         sum_tree::Summary::add_summary(self, summary, &());
     }
 }
 
-impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for Count {
-    fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) {
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
+    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
         self.0 += summary.count;
     }
 }
 
-impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for Count {
-    fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
-        self.0.cmp(&other.0)
-    }
-}
-
-impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for PendingCount {
-    fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) {
-        self.0 += summary.pending_count;
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
+    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+        self.0 += summary.rendered_count;
     }
 }
 
-impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for PendingCount {
-    fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
-        self.0.cmp(&other.0)
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
+    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+        self.0 += summary.unrendered_count;
     }
 }
 
-impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for Height {
-    fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) {
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
+    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
         self.0 += summary.height;
     }
 }
 
-impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for Height {
+impl<'a> sum_tree::SeekDimension<'a, ListItemSummary> for Height {
     fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
         self.0.partial_cmp(&other.0).unwrap()
     }
@@ -442,78 +556,89 @@ impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for Height {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{elements::*, geometry::vector::vec2f, Entity};
+    use crate::{elements::*, geometry::vector::vec2f, Entity, RenderContext, View};
 
     #[crate::test(self)]
     fn test_layout(cx: &mut crate::MutableAppContext) {
         let mut presenter = cx.build_presenter(0, 0.);
 
-        let mut elements = vec![20., 30., 100.];
-        let state = ListState::new(elements.len(), Orientation::Top);
+        let elements = Rc::new(RefCell::new(vec![20., 30., 100.]));
+        let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000, {
+            let elements = elements.clone();
+            move |ix, _| item(elements.borrow()[ix])
+        });
 
-        let mut list = List::new(
-            state.clone(),
-            &cx.build_render_context::<TestView>(0, 0, 0., false),
-            |range| elements[range].iter().copied().map(item),
-        )
-        .boxed();
+        let mut list = List::new(state.clone()).boxed();
         let size = list.layout(
             SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)),
             &mut presenter.build_layout_context(cx),
         );
         assert_eq!(size, vec2f(100., 40.));
         assert_eq!(
-            state.0.borrow().heights.summary(),
-            ElementHeightSummary {
+            state.0.borrow().items.summary(),
+            ListItemSummary {
                 count: 3,
-                pending_count: 0,
+                rendered_count: 3,
+                unrendered_count: 0,
                 height: 150.
             }
         );
 
         state.0.borrow_mut().scroll(
-            Default::default(),
+            &ScrollTop {
+                item_ix: 0,
+                offset_in_item: 0.,
+            },
+            40.,
             vec2f(0., 54.),
             true,
-            size.y(),
             &mut presenter.build_event_context(cx),
         );
-        assert_eq!(state.0.borrow().scroll_top, Some((2, 4.)));
+        assert_eq!(
+            state.0.borrow().scroll_top,
+            Some(ScrollTop {
+                item_ix: 2,
+                offset_in_item: 4.
+            })
+        );
         assert_eq!(state.0.borrow().scroll_top(size.y()), 54.);
 
-        elements.splice(1..2, vec![40., 50.]);
-        elements.push(60.);
+        elements.borrow_mut().splice(1..2, vec![40., 50.]);
+        elements.borrow_mut().push(60.);
         state.splice(1..2, 2);
         state.splice(4..4, 1);
         assert_eq!(
-            state.0.borrow().heights.summary(),
-            ElementHeightSummary {
+            state.0.borrow().items.summary(),
+            ListItemSummary {
                 count: 5,
-                pending_count: 3,
+                rendered_count: 2,
+                unrendered_count: 3,
                 height: 120.
             }
         );
 
-        let mut list = List::new(
-            state.clone(),
-            &cx.build_render_context::<TestView>(0, 0, 0., false),
-            |range| elements[range].iter().copied().map(item),
-        )
-        .boxed();
+        let mut list = List::new(state.clone()).boxed();
         let size = list.layout(
             SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)),
             &mut presenter.build_layout_context(cx),
         );
         assert_eq!(size, vec2f(100., 40.));
         assert_eq!(
-            state.0.borrow().heights.summary(),
-            ElementHeightSummary {
+            state.0.borrow().items.summary(),
+            ListItemSummary {
                 count: 5,
-                pending_count: 0,
+                rendered_count: 5,
+                unrendered_count: 0,
                 height: 270.
             }
         );
-        assert_eq!(state.0.borrow().scroll_top, Some((3, 4.)));
+        assert_eq!(
+            state.0.borrow().scroll_top,
+            Some(ScrollTop {
+                item_ix: 3,
+                offset_in_item: 4.
+            })
+        );
         assert_eq!(state.0.borrow().scroll_top(size.y()), 114.);
     }
 

gpui/src/presenter.rs 🔗

@@ -5,7 +5,8 @@ use crate::{
     json::{self, ToJson},
     platform::Event,
     text_layout::TextLayoutCache,
-    Action, AnyAction, AssetCache, ElementBox, FontSystem, Scene,
+    Action, AnyAction, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, ReadModel,
+    ReadView, Scene, View, ViewHandle,
 };
 use pathfinder_geometry::vector::{vec2f, Vector2F};
 use serde_json::json;
@@ -233,6 +234,32 @@ impl<'a> LayoutContext<'a> {
     }
 }
 
+impl<'a> Deref for LayoutContext<'a> {
+    type Target = MutableAppContext;
+
+    fn deref(&self) -> &Self::Target {
+        self.app
+    }
+}
+
+impl<'a> DerefMut for LayoutContext<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.app
+    }
+}
+
+impl<'a> ReadView for LayoutContext<'a> {
+    fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
+        self.app.read_view(handle)
+    }
+}
+
+impl<'a> ReadModel for LayoutContext<'a> {
+    fn read_model<T: Entity>(&self, handle: &ModelHandle<T>) -> &T {
+        self.app.read_model(handle)
+    }
+}
+
 pub struct PaintContext<'a> {
     rendered_views: &'a mut HashMap<usize, ElementBox>,
     pub scene: &'a mut Scene,

gpui/src/sum_tree.rs 🔗

@@ -10,7 +10,7 @@ const TREE_BASE: usize = 2;
 #[cfg(not(test))]
 const TREE_BASE: usize = 6;
 
-pub trait Item: Clone + fmt::Debug {
+pub trait Item: Clone {
     type Summary: Summary;
 
     fn summary(&self) -> Self::Summary;
@@ -87,6 +87,15 @@ impl<T: Item> SumTree<T> {
         tree
     }
 
+    pub fn from_iter<I: IntoIterator<Item = T>>(
+        iter: I,
+        cx: &<T::Summary as Summary>::Context,
+    ) -> Self {
+        let mut tree = Self::new();
+        tree.extend(iter, cx);
+        tree
+    }
+
     #[allow(unused)]
     pub fn items(&self, cx: &<T::Summary as Summary>::Context) -> Vec<T> {
         let mut items = Vec::new();

gpui/src/sum_tree/cursor.rs 🔗

@@ -10,6 +10,22 @@ struct StackEntry<'a, T: Item, S, U> {
     sum_dimension: U,
 }
 
+impl<'a, T, S, U> StackEntry<'a, T, S, U>
+where
+    T: Item,
+    S: SeekDimension<'a, T::Summary>,
+    U: SeekDimension<'a, T::Summary>,
+{
+    fn swap_dimensions(self) -> StackEntry<'a, T, U, S> {
+        StackEntry {
+            tree: self.tree,
+            index: self.index,
+            seek_dimension: self.sum_dimension,
+            sum_dimension: self.seek_dimension,
+        }
+    }
+}
+
 #[derive(Clone)]
 pub struct Cursor<'a, T: Item, S, U> {
     tree: &'a SumTree<T>,
@@ -602,6 +618,28 @@ where
     }
 }
 
+impl<'a, T, S, U> Cursor<'a, T, S, U>
+where
+    T: Item,
+    S: SeekDimension<'a, T::Summary>,
+    U: SeekDimension<'a, T::Summary>,
+{
+    pub fn swap_dimensions(self) -> Cursor<'a, T, U, S> {
+        Cursor {
+            tree: self.tree,
+            stack: self
+                .stack
+                .into_iter()
+                .map(StackEntry::swap_dimensions)
+                .collect(),
+            seek_dimension: self.sum_dimension,
+            sum_dimension: self.seek_dimension,
+            did_seek: self.did_seek,
+            at_end: self.at_end,
+        }
+    }
+}
+
 pub struct FilterCursor<'a, F: Fn(&T::Summary) -> bool, T: Item, U> {
     cursor: Cursor<'a, T, (), U>,
     filter_node: F,

zed/src/channel.rs 🔗

@@ -313,6 +313,12 @@ impl Channel {
         &self.messages
     }
 
+    pub fn message(&self, ix: usize) -> &ChannelMessage {
+        let mut cursor = self.messages.cursor::<Count, ()>();
+        cursor.seek(&Count(ix), Bias::Right, &());
+        cursor.item().unwrap()
+    }
+
     pub fn messages_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &ChannelMessage> {
         let mut cursor = self.messages.cursor::<Count, ()>();
         cursor.seek(&Count(range.start), Bias::Right, &());

zed/src/chat_panel.rs 🔗

@@ -70,10 +70,25 @@ impl ChatPanel {
                 }
             })
         });
+
+        let mut message_list = ListState::new(0, Orientation::Bottom, 100, {
+            let this = cx.handle().downgrade();
+            move |ix, cx| {
+                let this = this.upgrade(cx).unwrap().read(cx);
+                let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix);
+                this.render_message(message)
+            }
+        });
+        message_list.set_scroll_handler(|visible_range, cx| {
+            if visible_range.start < 5 {
+                cx.dispatch_action(LoadMoreMessages);
+            }
+        });
+
         let mut this = Self {
             channel_list,
             active_channel: Default::default(),
-            message_list: ListState::new(0, Orientation::Bottom),
+            message_list,
             input_editor,
             channel_select,
             settings,
@@ -120,14 +135,8 @@ impl ChatPanel {
 
     fn set_active_channel(&mut self, channel: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
         if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
+            self.message_list.reset(channel.read(cx).message_count());
             let subscription = cx.subscribe(&channel, Self::channel_did_change);
-            self.message_list =
-                ListState::new(channel.read(cx).message_count(), Orientation::Bottom);
-            self.message_list.set_scroll_handler(|visible_range, cx| {
-                if visible_range.start < 5 {
-                    cx.dispatch_action(LoadMoreMessages);
-                }
-            });
             self.active_channel = Some((channel, subscription));
         }
     }
@@ -149,16 +158,9 @@ impl ChatPanel {
         cx.notify();
     }
 
-    fn render_active_channel_messages(&self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let messages = if let Some((channel, _)) = self.active_channel.as_ref() {
-            let channel = channel.read(cx);
-            let now = OffsetDateTime::now_utc();
-            List::new(self.message_list.clone(), cx, |range| {
-                channel
-                    .messages_in_range(range)
-                    .map(|message| self.render_message(message, now))
-            })
-            .boxed()
+    fn render_active_channel_messages(&self) -> ElementBox {
+        let messages = if self.active_channel.is_some() {
+            List::new(self.message_list.clone()).boxed()
         } else {
             Empty::new().boxed()
         };
@@ -166,7 +168,8 @@ impl ChatPanel {
         Expanded::new(1., messages).boxed()
     }
 
-    fn render_message(&self, message: &ChannelMessage, now: OffsetDateTime) -> ElementBox {
+    fn render_message(&self, message: &ChannelMessage) -> ElementBox {
+        let now = OffsetDateTime::now_utc();
         let settings = self.settings.borrow();
         let theme = &settings.theme.chat_panel.message;
         Flex::column()
@@ -271,7 +274,7 @@ impl View for ChatPanel {
         "ChatPanel"
     }
 
-    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
         let theme = &self.settings.borrow().theme;
         Container::new(
             Flex::column()
@@ -280,7 +283,7 @@ impl View for ChatPanel {
                         .with_style(&theme.chat_panel.channel_select.container)
                         .boxed(),
                 )
-                .with_child(self.render_active_channel_messages(cx))
+                .with_child(self.render_active_channel_messages())
                 .with_child(self.render_input_box())
                 .boxed(),
         )