From 4fd3e220dbd39a0845c84e753864ca78926d083c Mon Sep 17 00:00:00 2001 From: "gcp-cherry-pick-bot[bot]" <98988430+gcp-cherry-pick-bot[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:42:16 +0530 Subject: [PATCH] gpui: Add `scroll_to_item_with_offset` to `UniformListScrollState` (cherry-pick #35064) (#35313) Cherry-picked gpui: Add `scroll_to_item_with_offset` to `UniformListScrollState` (#35064) Previously we had `ScrollStrategy::ToPosition(usize)` which lets you define the offset where you want to scroll that item to. This is the same as `ScrollStrategy::Top` but imagine some space reserved at the top. This PR removes `ScrollStrategy::ToPosition` in favor of `scroll_to_item_with_offset` which is the method to do the same. The reason to add this method is that now not just `ScrollStrategy::Top` but `ScrollStrategy::Center` can also uses this offset to center the item in the remaining unreserved space. ```rs // Before scroll_handle.scroll_to_item(index, ScrollStrategy::ToPosition(offset)); // After scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, offset); // New! Centers item skipping first x items scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Center, offset); ``` This will be useful for follow up PR. Release Notes: - N/A Co-authored-by: Smit Barmase --- crates/gpui/src/elements/uniform_list.rs | 64 ++++++++++++++++------- crates/project_panel/src/project_panel.rs | 5 +- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 52e2015c20..2ee6e9827d 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -88,15 +88,24 @@ pub enum ScrollStrategy { /// May not be possible if there's not enough list items above the item scrolled to: /// in this case, the element will be placed at the closest possible position. Center, - /// Scrolls the element to be at the given item index from the top of the viewport. - ToPosition(usize), +} + +#[derive(Clone, Copy, Debug)] +#[allow(missing_docs)] +pub struct DeferredScrollToItem { + /// The item index to scroll to + pub item_index: usize, + /// The scroll strategy to use + pub strategy: ScrollStrategy, + /// The offset in number of items + pub offset: usize, } #[derive(Clone, Debug, Default)] #[allow(missing_docs)] pub struct UniformListScrollState { pub base_handle: ScrollHandle, - pub deferred_scroll_to_item: Option<(usize, ScrollStrategy)>, + pub deferred_scroll_to_item: Option, /// Size of the item, captured during last layout. pub last_item_size: Option, /// Whether the list was vertically flipped during last layout. @@ -126,7 +135,24 @@ impl UniformListScrollHandle { /// Scroll the list to the given item index. pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) { - self.0.borrow_mut().deferred_scroll_to_item = Some((ix, strategy)); + self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { + item_index: ix, + strategy, + offset: 0, + }); + } + + /// Scroll the list to the given item index with an offset. + /// + /// For ScrollStrategy::Top, the item will be placed at the offset position from the top. + /// + /// For ScrollStrategy::Center, the item will be centered between offset and the last visible item. + pub fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) { + self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { + item_index: ix, + strategy, + offset, + }); } /// Check if the list is flipped vertically. @@ -139,7 +165,8 @@ impl UniformListScrollHandle { pub fn logical_scroll_top_index(&self) -> usize { let this = self.0.borrow(); this.deferred_scroll_to_item - .map(|(ix, _)| ix) + .as_ref() + .map(|deferred| deferred.item_index) .unwrap_or_else(|| this.base_handle.logical_scroll_top().0) } @@ -321,7 +348,8 @@ impl Element for UniformList { scroll_offset.x = Pixels::ZERO; } - if let Some((mut ix, scroll_strategy)) = shared_scroll_to_item { + if let Some(deferred_scroll) = shared_scroll_to_item { + let mut ix = deferred_scroll.item_index; if y_flipped { ix = self.item_count.saturating_sub(ix + 1); } @@ -330,23 +358,28 @@ impl Element for UniformList { let item_top = item_height * ix + padding.top; let item_bottom = item_top + item_height; let scroll_top = -updated_scroll_offset.y; + let offset_pixels = item_height * deferred_scroll.offset; let mut scrolled_to_top = false; - if item_top < scroll_top + padding.top { + + if item_top < scroll_top + padding.top + offset_pixels { scrolled_to_top = true; - updated_scroll_offset.y = -(item_top) + padding.top; + updated_scroll_offset.y = -(item_top) + padding.top + offset_pixels; } else if item_bottom > scroll_top + list_height - padding.bottom { scrolled_to_top = true; updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom; } - match scroll_strategy { + match deferred_scroll.strategy { ScrollStrategy::Top => {} ScrollStrategy::Center => { if scrolled_to_top { let item_center = item_top + item_height / 2.0; - let target_scroll_top = item_center - list_height / 2.0; - if item_top < scroll_top + let viewport_height = list_height - offset_pixels; + let viewport_center = offset_pixels + viewport_height / 2.0; + let target_scroll_top = item_center - viewport_center; + + if item_top < scroll_top + offset_pixels || item_bottom > scroll_top + list_height { updated_scroll_offset.y = -target_scroll_top @@ -356,15 +389,6 @@ impl Element for UniformList { } } } - ScrollStrategy::ToPosition(sticky_index) => { - let target_y_in_viewport = item_height * sticky_index; - let target_scroll_top = item_top - target_y_in_viewport; - let max_scroll_top = - (content_height - list_height).max(Pixels::ZERO); - let new_scroll_top = - target_scroll_top.clamp(Pixels::ZERO, max_scroll_top); - updated_scroll_offset.y = -new_scroll_top; - } } scroll_offset = *updated_scroll_offset } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b0073f294f..82dc45b7bf 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4226,10 +4226,7 @@ impl ProjectPanel { this.marked_entries.clear(); if is_sticky { if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) { - let strategy = sticky_index - .map(ScrollStrategy::ToPosition) - .unwrap_or(ScrollStrategy::Top); - this.scroll_handle.scroll_to_item(index, strategy); + this.scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0)); cx.notify(); // move down by 1px so that clicked item // don't count as sticky anymore