Implement scrolling and painting for `List`

Antonio Scandurra and Nathan Sobo created

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

Change summary

gpui/src/elements/list.rs | 129 +++++++++++++++++++++++++++++++++-------
1 file changed, 105 insertions(+), 24 deletions(-)

Detailed changes

gpui/src/elements/list.rs 🔗

@@ -1,7 +1,11 @@
 use crate::{
-    geometry::{rect::RectF, vector::Vector2F},
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
+    json::json,
     sum_tree::{self, Bias, SumTree},
-    Element,
+    DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
 };
 use parking_lot::Mutex;
 use std::{ops::Range, sync::Arc};
@@ -19,6 +23,7 @@ struct StateInner {
     last_layout_width: f32,
     elements: Vec<ElementBox>,
     heights: SumTree<ElementHeight>,
+    scroll_top: f32,
 }
 
 #[derive(Clone, Debug)]
@@ -47,6 +52,25 @@ impl List {
     pub fn new(state: ListState) -> Self {
         Self { state }
     }
+
+    fn scroll(
+        &self,
+        _: Vector2F,
+        delta: Vector2F,
+        precise: bool,
+        scroll_max: f32,
+        cx: &mut EventContext,
+    ) -> bool {
+        if !precise {
+            todo!("still need to handle non-precise scroll events from a mouse wheel");
+        }
+
+        let mut state = self.state.0.lock();
+        state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
+        cx.notify();
+
+        true
+    }
 }
 
 impl Element for List {
@@ -56,8 +80,8 @@ impl Element for List {
 
     fn layout(
         &mut self,
-        constraint: crate::SizeConstraint,
-        cx: &mut crate::LayoutContext,
+        constraint: SizeConstraint,
+        cx: &mut LayoutContext,
     ) -> (Vector2F, Self::LayoutState) {
         let state = &mut *self.state.0.lock();
         let mut item_constraint = constraint;
@@ -100,34 +124,69 @@ impl Element for List {
         (constraint.max, ())
     }
 
-    fn paint(
-        &mut self,
-        bounds: RectF,
-        layout: &mut Self::LayoutState,
-        cx: &mut crate::PaintContext,
-    ) -> Self::PaintState {
-        todo!()
+    fn paint(&mut self, bounds: RectF, _: &mut (), cx: &mut PaintContext) {
+        let state = &mut *self.state.0.lock();
+        let visible_range = state.visible_range(bounds.height());
+
+        let mut item_top = {
+            let mut cursor = state.heights.cursor::<Count, Height>();
+            cursor.seek(&Count(visible_range.start), Bias::Right, &());
+            cursor.sum_start().0
+        };
+        for element in &mut state.elements[visible_range] {
+            let origin = bounds.origin() + vec2f(0., item_top) - state.scroll_top;
+            element.paint(origin, cx);
+            item_top += element.size().y();
+        }
     }
 
     fn dispatch_event(
         &mut self,
-        event: &crate::Event,
+        event: &Event,
         bounds: RectF,
-        layout: &mut Self::LayoutState,
-        paint: &mut Self::PaintState,
-        cx: &mut crate::EventContext,
+        _: &mut (),
+        _: &mut (),
+        cx: &mut EventContext,
     ) -> bool {
-        todo!()
+        let mut handled = false;
+
+        let mut state = self.state.0.lock();
+        let visible_range = state.visible_range(bounds.height());
+        for item in &mut state.elements[visible_range] {
+            handled = item.dispatch_event(event, cx) || handled;
+        }
+
+        match event {
+            Event::ScrollWheel {
+                position,
+                delta,
+                precise,
+            } => {
+                if bounds.contains_point(*position) {
+                    let scroll_max = state.scroll_max(bounds.height());
+                    if self.scroll(*position, *delta, *precise, scroll_max, cx) {
+                        handled = true;
+                    }
+                }
+            }
+            _ => {}
+        }
+
+        handled
     }
 
-    fn debug(
-        &self,
-        bounds: RectF,
-        layout: &Self::LayoutState,
-        paint: &Self::PaintState,
-        cx: &crate::DebugContext,
-    ) -> serde_json::Value {
-        todo!()
+    fn debug(&self, bounds: RectF, _: &(), _: &(), cx: &DebugContext) -> serde_json::Value {
+        let state = self.state.0.lock();
+        let visible_range = state.visible_range(bounds.height());
+        let visible_elements = state.elements[visible_range.clone()]
+            .iter()
+            .map(|e| e.debug(cx))
+            .collect::<Vec<_>>();
+        json!({
+            "visible_range": visible_range,
+            "visible_elements": visible_elements,
+            "scroll_top": state.scroll_top,
+        })
     }
 }
 
@@ -139,6 +198,7 @@ impl ListState {
             last_layout_width: 0.,
             elements,
             heights,
+            scroll_top: 0.,
         })))
     }
 
@@ -170,6 +230,21 @@ 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), Bias::Right, &());
+        let start_ix = cursor.sum_start().0;
+        cursor.seek(&Height(self.scroll_top + height), Bias::Left, &());
+        let end_ix = cursor.sum_start().0;
+        start_ix..end_ix + 1
+    }
+
+    fn scroll_max(&self, height: f32) -> f32 {
+        self.heights.summary().height - height
+    }
+}
+
 impl ElementHeight {
     fn is_pending(&self) -> bool {
         matches!(self, ElementHeight::Pending)
@@ -241,6 +316,12 @@ impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for Height {
     }
 }
 
+impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for Height {
+    fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
+        self.0.partial_cmp(&other.0).unwrap()
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;