Add a ScrollHandle to gpui2 for the collab panel

Conrad Irwin created

Change summary

crates/collab_ui2/src/collab_panel.rs | 107 ++++++++++++++++------------
crates/gpui2/src/elements/div.rs      |  63 +++++++++++++++++
2 files changed, 124 insertions(+), 46 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -172,8 +172,8 @@ use gpui::{
     actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext,
     AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle,
     Focusable, FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent,
-    ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, SharedString, Stateful, Styled,
-    Subscription, Task, View, ViewContext, VisualContext, WeakView,
+    ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, ScrollHandle, SharedString,
+    Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
 };
 use project::{Fs, Project};
 use serde_derive::{Deserialize, Serialize};
@@ -302,6 +302,7 @@ pub struct CollabPanel {
     client: Arc<Client>,
     project: Model<Project>,
     match_candidates: Vec<StringMatchCandidate>,
+    scroll_handle: ScrollHandle,
     // list_state: ListState<Self>,
     subscriptions: Vec<Subscription>,
     collapsed_sections: Vec<Section>,
@@ -586,6 +587,7 @@ impl CollabPanel {
                 project: workspace.project().clone(),
                 subscriptions: Vec::default(),
                 match_candidates: Vec::default(),
+                scroll_handle: ScrollHandle::new(),
                 collapsed_sections: vec![Section::Offline],
                 collapsed_channels: Vec::default(),
                 workspace: workspace.weak_handle(),
@@ -2348,48 +2350,67 @@ impl CollabPanel {
     }
 
     fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
-        div()
+        dbg!(&self.scroll_handle.top_item());
+
+        v_stack()
+            .size_full()
             .child(
                 div()
-                    .m_2()
-                    .rounded(px(2.0))
-                    .child(self.filter_editor.clone()),
+                    .p_2()
+                    .child(div().rounded(px(2.0)).child(self.filter_editor.clone())),
             )
             .child(
-                List::new().children(self.entries.clone().into_iter().enumerate().map(
-                    |(ix, entry)| {
-                        let is_selected = self.selection == Some(ix);
-                        match entry {
-                            ListEntry::Header(section) => {
-                                let is_collapsed = self.collapsed_sections.contains(&section);
-                                self.render_header(section, is_selected, is_collapsed, cx)
-                                    .into_any_element()
-                            }
-                            ListEntry::Contact { contact, calling } => self
-                                .render_contact(&*contact, calling, is_selected, cx)
-                                .into_any_element(),
-                            ListEntry::ContactPlaceholder => self
-                                .render_contact_placeholder(is_selected, cx)
-                                .into_any_element(),
-                            ListEntry::IncomingRequest(user) => self
-                                .render_contact_request(user, true, is_selected, cx)
-                                .into_any_element(),
-                            ListEntry::OutgoingRequest(user) => self
-                                .render_contact_request(user, false, is_selected, cx)
-                                .into_any_element(),
-                            ListEntry::Channel {
-                                channel,
-                                depth,
-                                has_children,
-                            } => self
-                                .render_channel(&*channel, depth, has_children, is_selected, ix, cx)
-                                .into_any_element(),
-                            ListEntry::ChannelEditor { depth } => {
-                                self.render_channel_editor(depth, cx).into_any_element()
-                            }
-                        }
-                    },
-                )),
+                v_stack()
+                    .size_full()
+                    .id("scroll")
+                    .overflow_y_scroll()
+                    .track_scroll(&self.scroll_handle)
+                    .children(
+                        self.entries
+                            .clone()
+                            .into_iter()
+                            .enumerate()
+                            .map(|(ix, entry)| {
+                                let is_selected = self.selection == Some(ix);
+                                match entry {
+                                    ListEntry::Header(section) => {
+                                        let is_collapsed =
+                                            self.collapsed_sections.contains(&section);
+                                        self.render_header(section, is_selected, is_collapsed, cx)
+                                            .into_any_element()
+                                    }
+                                    ListEntry::Contact { contact, calling } => self
+                                        .render_contact(&*contact, calling, is_selected, cx)
+                                        .into_any_element(),
+                                    ListEntry::ContactPlaceholder => self
+                                        .render_contact_placeholder(is_selected, cx)
+                                        .into_any_element(),
+                                    ListEntry::IncomingRequest(user) => self
+                                        .render_contact_request(user, true, is_selected, cx)
+                                        .into_any_element(),
+                                    ListEntry::OutgoingRequest(user) => self
+                                        .render_contact_request(user, false, is_selected, cx)
+                                        .into_any_element(),
+                                    ListEntry::Channel {
+                                        channel,
+                                        depth,
+                                        has_children,
+                                    } => self
+                                        .render_channel(
+                                            &*channel,
+                                            depth,
+                                            has_children,
+                                            is_selected,
+                                            ix,
+                                            cx,
+                                        )
+                                        .into_any_element(),
+                                    ListEntry::ChannelEditor { depth } => {
+                                        self.render_channel_editor(depth, cx).into_any_element()
+                                    }
+                                }
+                            }),
+                    ),
             )
     }
 
@@ -3249,12 +3270,6 @@ impl Render for CollabPanel {
             } else {
                 self.render_signed_in(cx)
             })
-            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
-                overlay()
-                    .position(*position)
-                    .anchor(gpui::AnchorCorner::TopLeft)
-                    .child(menu.clone())
-            }))
     }
 }
 

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

@@ -12,6 +12,7 @@ use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
     cell::RefCell,
+    cmp::Ordering,
     fmt::Debug,
     mem,
     rc::Rc,
@@ -357,6 +358,11 @@ pub trait StatefulInteractiveElement: InteractiveElement {
         self
     }
 
+    fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
+        self.interactivity().scroll_handle = Some(scroll_handle.clone());
+        self
+    }
+
     fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
     where
         Self: Sized,
@@ -626,6 +632,18 @@ impl Element for Div {
         let mut child_max = Point::default();
         let content_size = if element_state.child_layout_ids.is_empty() {
             bounds.size
+        } else if let Some(scroll_handle) = self.interactivity.scroll_handle.as_ref() {
+            let mut state = scroll_handle.0.borrow_mut();
+            state.child_bounds = Vec::with_capacity(element_state.child_layout_ids.len());
+            state.bounds = bounds;
+
+            for child_layout_id in &element_state.child_layout_ids {
+                let child_bounds = cx.layout_bounds(*child_layout_id);
+                child_min = child_min.min(&child_bounds.origin);
+                child_max = child_max.max(&child_bounds.lower_right());
+                state.child_bounds.push(child_bounds)
+            }
+            (child_max - child_min).into()
         } else {
             for child_layout_id in &element_state.child_layout_ids {
                 let child_bounds = cx.layout_bounds(*child_layout_id);
@@ -696,6 +714,7 @@ pub struct Interactivity {
     pub key_context: KeyContext,
     pub focusable: bool,
     pub tracked_focus_handle: Option<FocusHandle>,
+    pub scroll_handle: Option<ScrollHandle>,
     pub focus_listeners: FocusListeners,
     pub group: Option<SharedString>,
     pub base_style: StyleRefinement,
@@ -754,6 +773,10 @@ impl Interactivity {
             });
         }
 
+        if let Some(scroll_handle) = self.scroll_handle.as_ref() {
+            element_state.scroll_offset = Some(scroll_handle.0.borrow().offset.clone());
+        }
+
         let style = self.compute_style(None, &mut element_state, cx);
         let layout_id = f(style, cx);
         (layout_id, element_state)
@@ -1206,6 +1229,7 @@ impl Default for Interactivity {
             key_context: KeyContext::default(),
             focusable: false,
             tracked_focus_handle: None,
+            scroll_handle: None,
             focus_listeners: SmallVec::default(),
             // scroll_offset: Point::default(),
             group: None,
@@ -1429,3 +1453,42 @@ where
         self.element.children_mut()
     }
 }
+
+#[derive(Default)]
+struct ScrollHandleState {
+    // not great to have the nested rc's...
+    offset: Rc<RefCell<Point<Pixels>>>,
+    bounds: Bounds<Pixels>,
+    child_bounds: Vec<Bounds<Pixels>>,
+}
+
+#[derive(Clone)]
+pub struct ScrollHandle(Rc<RefCell<ScrollHandleState>>);
+
+impl ScrollHandle {
+    pub fn new() -> Self {
+        Self(Rc::default())
+    }
+
+    pub fn offset(&self) -> Point<Pixels> {
+        self.0.borrow().offset.borrow().clone()
+    }
+
+    pub fn top_item(&self) -> usize {
+        let state = self.0.borrow();
+        let top = state.bounds.top() - state.offset.borrow().y;
+
+        match state.child_bounds.binary_search_by(|bounds| {
+            if top < bounds.top() {
+                Ordering::Greater
+            } else if top > bounds.bottom() {
+                Ordering::Less
+            } else {
+                Ordering::Equal
+            }
+        }) {
+            Ok(ix) => ix,
+            Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)),
+        }
+    }
+}