project panel: Respect y offset of a click-and-drag on a scrollbar (#13506)

Piotr Osiewicz created

Previously we've always snapped the scrollbar to the cursor position,
without regard for the position of the thumb.



Release Notes:

- N/A

Change summary

crates/project_panel/src/project_panel.rs | 10 ++++----
crates/project_panel/src/scrollbar.rs     | 26 ++++++++++++++----------
2 files changed, 20 insertions(+), 16 deletions(-)

Detailed changes

crates/project_panel/src/project_panel.rs 🔗

@@ -72,7 +72,7 @@ pub struct ProjectPanel {
     width: Option<Pixels>,
     pending_serialization: Task<Option<()>>,
     show_scrollbar: bool,
-    is_dragging_scrollbar: Rc<Cell<bool>>,
+    scrollbar_drag_thumb_offset: Rc<Cell<Option<f32>>>,
     hide_scrollbar_task: Option<Task<()>>,
 }
 
@@ -289,7 +289,7 @@ impl ProjectPanel {
                 pending_serialization: Task::ready(None),
                 show_scrollbar: !Self::should_autohide_scrollbar(cx),
                 hide_scrollbar_task: None,
-                is_dragging_scrollbar: Default::default(),
+                scrollbar_drag_thumb_offset: Default::default(),
             };
             this.update_visible_entries(None, cx);
 
@@ -2231,7 +2231,7 @@ impl ProjectPanel {
 
         let height = scroll_handle
             .last_item_height
-            .filter(|_| self.show_scrollbar || self.is_dragging_scrollbar.get())?;
+            .filter(|_| self.show_scrollbar || self.scrollbar_drag_thumb_offset.get().is_some())?;
 
         let total_list_length = height.0 as f64 * items_count as f64;
         let current_offset = scroll_handle.base_handle.offset().y.0.min(0.).abs() as f64;
@@ -2270,7 +2270,7 @@ impl ProjectPanel {
                 .on_mouse_up(
                     MouseButton::Left,
                     cx.listener(|this, _, cx| {
-                        if !this.is_dragging_scrollbar.get()
+                        if this.scrollbar_drag_thumb_offset.get().is_none()
                             && !this.focus_handle.contains_focused(cx)
                         {
                             this.hide_scrollbar(cx);
@@ -2293,7 +2293,7 @@ impl ProjectPanel {
                 .child(ProjectPanelScrollbar::new(
                     percentage as f32..end_offset as f32,
                     self.scroll_handle.clone(),
-                    self.is_dragging_scrollbar.clone(),
+                    self.scrollbar_drag_thumb_offset.clone(),
                     cx.view().clone().into(),
                     items_count,
                 )),

crates/project_panel/src/scrollbar.rs 🔗

@@ -9,7 +9,8 @@ use ui::{prelude::*, px, relative, IntoElement};
 pub(crate) struct ProjectPanelScrollbar {
     thumb: Range<f32>,
     scroll: UniformListScrollHandle,
-    is_dragging_scrollbar: Rc<Cell<bool>>,
+    // If Some(), there's an active drag, offset by percentage from the top of thumb.
+    scrollbar_drag_state: Rc<Cell<Option<f32>>>,
     item_count: usize,
     view: AnyView,
 }
@@ -18,14 +19,14 @@ impl ProjectPanelScrollbar {
     pub(crate) fn new(
         thumb: Range<f32>,
         scroll: UniformListScrollHandle,
-        is_dragging_scrollbar: Rc<Cell<bool>>,
+        scrollbar_drag_state: Rc<Cell<Option<f32>>>,
         view: AnyView,
         item_count: usize,
     ) -> Self {
         Self {
             thumb,
             scroll,
-            is_dragging_scrollbar,
+            scrollbar_drag_state,
             item_count,
             view,
         }
@@ -97,7 +98,7 @@ impl gpui::Element for ProjectPanelScrollbar {
             let item_count = self.item_count;
             cx.on_mouse_event({
                 let scroll = self.scroll.clone();
-                let is_dragging = self.is_dragging_scrollbar.clone();
+                let is_dragging = self.scrollbar_drag_state.clone();
                 move |event: &MouseDownEvent, phase, _cx| {
                     if phase.bubble() && bounds.contains(&event.position) {
                         if !thumb_bounds.contains(&event.position) {
@@ -113,7 +114,9 @@ impl gpui::Element for ProjectPanelScrollbar {
                                     .set_offset(point(px(0.), -max_offset * percentage));
                             }
                         } else {
-                            is_dragging.set(true);
+                            let thumb_top_offset =
+                                (event.position.y - thumb_bounds.origin.y) / bounds.size.height;
+                            is_dragging.set(Some(thumb_top_offset));
                         }
                     }
                 }
@@ -130,14 +133,15 @@ impl gpui::Element for ProjectPanelScrollbar {
                     }
                 }
             });
-            let is_dragging = self.is_dragging_scrollbar.clone();
+            let drag_state = self.scrollbar_drag_state.clone();
             let view_id = self.view.entity_id();
             cx.on_mouse_event(move |event: &MouseMoveEvent, _, cx| {
-                if event.dragging() && is_dragging.get() {
+                if let Some(drag_state) = drag_state.get().filter(|_| event.dragging()) {
                     let scroll = scroll.0.borrow();
                     if let Some(last_height) = scroll.last_item_height {
                         let max_offset = item_count as f32 * last_height;
-                        let percentage = (event.position.y - bounds.origin.y) / bounds.size.height;
+                        let percentage =
+                            (event.position.y - bounds.origin.y) / bounds.size.height - drag_state;
 
                         let percentage = percentage.min(1. - thumb_percentage_size);
                         scroll
@@ -146,13 +150,13 @@ impl gpui::Element for ProjectPanelScrollbar {
                         cx.notify(view_id);
                     }
                 } else {
-                    is_dragging.set(false);
+                    drag_state.set(None);
                 }
             });
-            let is_dragging = self.is_dragging_scrollbar.clone();
+            let is_dragging = self.scrollbar_drag_state.clone();
             cx.on_mouse_event(move |_event: &MouseUpEvent, phase, cx| {
                 if phase.bubble() {
-                    is_dragging.set(false);
+                    is_dragging.set(None);
                     cx.notify(view_id);
                 }
             });