Implement list scroll tracking

Max Brunsfeld and Mikayla created

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

crates/gpui2/src/elements/list.rs | 53 ++++++++++++++++++++++++++++++--
crates/gpui2/src/interactive.rs   |  6 +++
crates/picker2/src/picker2.rs     | 45 ++++++++++++---------------
3 files changed, 75 insertions(+), 29 deletions(-)

Detailed changes

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

@@ -1,11 +1,12 @@
 use crate::{
     point, px, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, ElementId,
-    ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Size, StatefulInteractive,
-    StatefulInteractivity, StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled,
-    ViewContext,
+    ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size,
+    StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity,
+    StyleRefinement, Styled, ViewContext,
 };
+use parking_lot::Mutex;
 use smallvec::SmallVec;
-use std::{cmp, ops::Range};
+use std::{cmp, ops::Range, sync::Arc};
 use taffy::style::Overflow;
 
 pub fn list<Id, V, C>(
@@ -30,6 +31,7 @@ where
                 .collect()
         }),
         interactivity: id.into(),
+        scroll_handle: None,
     }
 }
 
@@ -45,6 +47,37 @@ pub struct List<V: 'static> {
         ) -> SmallVec<[AnyElement<V>; 64]>,
     >,
     interactivity: StatefulInteractivity<V>,
+    scroll_handle: Option<ListScrollHandle>,
+}
+
+#[derive(Clone)]
+pub struct ListScrollHandle(Arc<Mutex<Option<ListScrollHandleState>>>);
+
+#[derive(Clone, Debug)]
+struct ListScrollHandleState {
+    item_height: Pixels,
+    list_height: Pixels,
+    scroll_offset: Arc<Mutex<Point<Pixels>>>,
+}
+
+impl ListScrollHandle {
+    pub fn new() -> Self {
+        Self(Arc::new(Mutex::new(None)))
+    }
+
+    pub fn scroll_to_item(&self, ix: usize) {
+        if let Some(state) = &*self.0.lock() {
+            let mut scroll_offset = state.scroll_offset.lock();
+            let item_top = state.item_height * ix;
+            let item_bottom = item_top + state.item_height;
+            let scroll_top = -scroll_offset.y;
+            if item_top < scroll_top {
+                scroll_offset.y = -item_top;
+            } else if item_bottom > scroll_top + state.list_height {
+                scroll_offset.y = -(item_bottom - state.list_height);
+            }
+        }
+    }
 }
 
 #[derive(Default)]
@@ -107,6 +140,13 @@ impl<V: 'static> Element<V> for List<V> {
             let content_size;
             if self.item_count > 0 {
                 let item_height = self.measure_item_height(view_state, padded_bounds, cx);
+                if let Some(scroll_handle) = self.scroll_handle.clone() {
+                    scroll_handle.0.lock().replace(ListScrollHandleState {
+                        item_height,
+                        list_height: padded_bounds.size.height,
+                        scroll_offset: element_state.interactive.track_scroll_offset(),
+                    });
+                }
                 let visible_item_count =
                     (padded_bounds.size.height / item_height).ceil() as usize + 1;
                 let scroll_offset = element_state
@@ -187,6 +227,11 @@ impl<V> List<V> {
         );
         cx.layout_bounds(layout_id).size.height
     }
+
+    pub fn track_scroll(mut self, handle: ListScrollHandle) -> Self {
+        self.scroll_handle = Some(handle);
+        self
+    }
 }
 
 impl<V: 'static> StatelessInteractive<V> for List<V> {

crates/gpui2/src/interactive.rs 🔗

@@ -900,6 +900,12 @@ impl InteractiveElementState {
             .as_ref()
             .map(|offset| offset.lock().clone())
     }
+
+    pub fn track_scroll_offset(&mut self) -> Arc<Mutex<Point<Pixels>>> {
+        self.scroll_offset
+            .get_or_insert_with(|| Arc::new(Mutex::new(Default::default())))
+            .clone()
+    }
 }
 
 impl<V> Default for StatelessInteractivity<V> {

crates/picker2/src/picker2.rs 🔗

@@ -1,28 +1,8 @@
-// use editor::Editor;
-// use gpui::{
-//     elements::*,
-//     geometry::vector::{vec2f, Vector2F},
-//     keymap_matcher::KeymapContext,
-//     platform::{CursorStyle, MouseButton},
-//     AnyElement, AnyViewHandle, AppContext, Axis, Entity, MouseState, Task, View, ViewContext,
-//     ViewHandle,
-// };
-// use menu::{Cancel, Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
-// use parking_lot::Mutex;
-// use std::{cmp, sync::Arc};
-// use util::ResultExt;
-// use workspace::Modal;
-
-// #[derive(Clone, Copy)]
-// pub enum PickerEvent {
-//     Dismiss,
-// }
-
 use std::cmp;
 
 use gpui::{
-    div, list, Component, ElementId, FocusHandle, Focusable, ParentElement, StatelessInteractive,
-    Styled, ViewContext,
+    div, list, Component, ElementId, FocusHandle, Focusable, ListScrollHandle, ParentElement,
+    StatelessInteractive, Styled, ViewContext,
 };
 
 // pub struct Picker<D> {
@@ -99,6 +79,7 @@ impl<V: PickerDelegate> Picker<V> {
 impl<V: 'static + PickerDelegate> Picker<V> {
     pub fn render(self, view: &mut V, _cx: &mut ViewContext<V>) -> impl Component<V> {
         let id = self.id.clone();
+        let scroll_handle = ListScrollHandle::new();
         div()
             .size_full()
             .id(self.id.clone())
@@ -112,36 +93,49 @@ impl<V: 'static + PickerDelegate> Picker<V> {
             })
             .on_action({
                 let id = id.clone();
+                let scroll_handle = scroll_handle.clone();
                 move |view: &mut V, _: &menu::SelectNext, cx| {
                     let count = view.match_count(id.clone());
                     if count > 0 {
                         let index = view.selected_index(id.clone());
-                        view.set_selected_index(cmp::min(index + 1, count - 1), id.clone(), cx);
+                        let ix = cmp::min(index + 1, count - 1);
+                        view.set_selected_index(ix, id.clone(), cx);
+                        scroll_handle.scroll_to_item(ix);
                     }
                 }
             })
             .on_action({
                 let id = id.clone();
+                let scroll_handle = scroll_handle.clone();
                 move |view, _: &menu::SelectPrev, cx| {
                     let count = view.match_count(id.clone());
                     if count > 0 {
                         let index = view.selected_index(id.clone());
-                        view.set_selected_index(index.saturating_sub(1), id.clone(), cx);
+                        let ix = index.saturating_sub(1);
+                        view.set_selected_index(ix, id.clone(), cx);
+                        scroll_handle.scroll_to_item(ix);
                     }
                 }
             })
             .on_action({
                 let id = id.clone();
+                let scroll_handle = scroll_handle.clone();
                 move |view: &mut V, _: &menu::SelectFirst, cx| {
-                    view.set_selected_index(0, id.clone(), cx);
+                    let count = view.match_count(id.clone());
+                    if count > 0 {
+                        view.set_selected_index(0, id.clone(), cx);
+                        scroll_handle.scroll_to_item(0);
+                    }
                 }
             })
             .on_action({
                 let id = id.clone();
+                let scroll_handle = scroll_handle.clone();
                 move |view: &mut V, _: &menu::SelectLast, cx| {
                     let count = view.match_count(id.clone());
                     if count > 0 {
                         view.set_selected_index(count - 1, id.clone(), cx);
+                        scroll_handle.scroll_to_item(count - 1);
                     }
                 }
             })
@@ -174,6 +168,7 @@ impl<V: 'static + PickerDelegate> Picker<V> {
                             .collect()
                     },
                 )
+                .track_scroll(scroll_handle.clone())
                 .size_full(),
             )
     }