From 2965119201067d726a1237cae268910d7bc745cd Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:56:58 +0100 Subject: [PATCH] project panel: scroll when drag-hovering over the edge of a list (#20207) Closes [19554](https://github.com/zed-industries/zed/issues/19554) Release Notes: - Added auto-scrolling to project panel when a vertical edge of a panel is hovered with a dragged entry. --- crates/project_panel/src/project_panel.rs | 65 +++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 71a2f96624973bdcc08ca773739c42318952a648..72388187f4dfcf884962cfe25f6844fe952baacc 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -63,6 +63,9 @@ pub struct ProjectPanel { fs: Arc, focus_handle: FocusHandle, scroll_handle: UniformListScrollHandle, + // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's + // hovered over the start/end of a list. + hover_scroll_task: Option>, visible_entries: Vec<(WorktreeId, Vec, OnceCell>>)>, /// Maps from leaf project entry ID to the currently selected ancestor. /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several @@ -307,6 +310,7 @@ impl ProjectPanel { let scroll_handle = UniformListScrollHandle::new(); let mut this = Self { project: project.clone(), + hover_scroll_task: None, fs: workspace.app_state().fs.clone(), focus_handle, visible_entries: Default::default(), @@ -2544,6 +2548,7 @@ impl ProjectPanel { )) .on_drop(cx.listener( move |this, external_paths: &ExternalPaths, cx| { + this.hover_scroll_task.take(); this.last_external_paths_drag_over_entry = None; this.marked_entries.clear(); this.drop_external_files(external_paths.paths(), entry_id, cx); @@ -2563,6 +2568,7 @@ impl ProjectPanel { style.bg(cx.theme().colors().drop_target_background) }) .on_drop(cx.listener(move |this, selections: &DraggedSelection, cx| { + this.hover_scroll_task.take(); this.drag_onto(selections, entry_id, kind.is_file(), cx); })) .child( @@ -3057,9 +3063,67 @@ impl Render for ProjectPanel { .map(|(_, worktree_entries, _)| worktree_entries.len()) .sum(); + fn handle_drag_move_scroll( + this: &mut ProjectPanel, + e: &DragMoveEvent, + cx: &mut ViewContext, + ) { + if !e.bounds.contains(&e.event.position) { + return; + } + this.hover_scroll_task.take(); + let panel_height = e.bounds.size.height; + if panel_height <= px(0.) { + return; + } + + let event_offset = e.event.position.y - e.bounds.origin.y; + // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom) + let hovered_region_offset = event_offset / panel_height; + + // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list. + // These pixels offsets were picked arbitrarily. + let vertical_scroll_offset = if hovered_region_offset <= 0.05 { + 8. + } else if hovered_region_offset <= 0.15 { + 5. + } else if hovered_region_offset >= 0.95 { + -8. + } else if hovered_region_offset >= 0.85 { + -5. + } else { + return; + }; + let adjustment = point(px(0.), px(vertical_scroll_offset)); + this.hover_scroll_task = Some(cx.spawn(move |this, mut cx| async move { + loop { + let should_stop_scrolling = this + .update(&mut cx, |this, cx| { + this.hover_scroll_task.as_ref()?; + let handle = this.scroll_handle.0.borrow_mut(); + let offset = handle.base_handle.offset(); + + handle.base_handle.set_offset(offset + adjustment); + cx.notify(); + Some(()) + }) + .ok() + .flatten() + .is_some(); + if should_stop_scrolling { + return; + } + cx.background_executor() + .timer(Duration::from_millis(16)) + .await; + } + })); + } h_flex() .id("project-panel") .group("project-panel") + .on_drag_move(cx.listener(handle_drag_move_scroll::)) + .on_drag_move(cx.listener(handle_drag_move_scroll::)) .size_full() .relative() .on_hover(cx.listener(|this, hovered, cx| { @@ -3291,6 +3355,7 @@ impl Render for ProjectPanel { move |this, external_paths: &ExternalPaths, cx| { this.last_external_paths_drag_over_entry = None; this.marked_entries.clear(); + this.hover_scroll_task.take(); if let Some(task) = this .workspace .update(cx, |workspace, cx| {