virtual_list: Fix scroll to item not align to paddings. (#1122)

- Fixed List scroll to item at first time rendering.
This commit is contained in:
Jason Lee 2025-08-08 11:56:18 +08:00 committed by GitHub
parent 123a4a9fcd
commit 880d023834
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 126 additions and 97 deletions

View file

@ -89,7 +89,7 @@ impl DropdownStory {
let country_dropdown = cx.new(|cx| {
DropdownState::new(
grouped_countries,
Some(IndexPath::default().row(1)),
Some(IndexPath::default().row(8).section(2)),
window,
cx,
)

View file

@ -556,7 +556,7 @@ where
cx: &mut Context<Self>,
) {
self.list.update(cx, |list, cx| {
list.set_selected_index(selected_index, window, cx);
list._set_selected_index(selected_index, window, cx);
});
self.update_selected_value(window, cx);
}

View file

@ -60,6 +60,7 @@ pub struct List<D: ListDelegate> {
pub(crate) size: Size,
rows_cache: RowsCache,
selected_index: Option<IndexPath>,
deferred_scroll_to_index: Option<IndexPath>,
mouse_right_clicked_index: Option<IndexPath>,
reset_on_cancel: bool,
_search_task: Task<()>,
@ -85,6 +86,7 @@ where
query_input: Some(query_input),
last_query: None,
selected_index: None,
deferred_scroll_to_index: None,
mouse_right_clicked_index: None,
scroll_handle: VirtualListScrollHandle::new(),
scroll_state: ScrollbarState::default(),
@ -158,7 +160,7 @@ where
/// Set the selected index of the list,
/// this will also scroll to the selected item.
fn _set_selected_index(
pub(crate) fn _set_selected_index(
&mut self,
ix: Option<IndexPath>,
window: &mut Window,
@ -213,10 +215,8 @@ where
return;
}
if let Some(ix) = self.rows_cache.position_of(&ix) {
self.scroll_handle.scroll_to_item(ix, ScrollStrategy::Top);
cx.notify();
}
self.deferred_scroll_to_index = Some(ix);
cx.notify();
}
/// Get scroll handle
@ -226,11 +226,8 @@ where
pub fn scroll_to_selected_item(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
if let Some(ix) = self.selected_index {
if let Some(item_ix) = self.rows_cache.position_of(&ix) {
self.scroll_handle
.scroll_to_item(item_ix, ScrollStrategy::Top);
cx.notify();
}
self.deferred_scroll_to_index = Some(ix);
cx.notify();
}
}
@ -575,6 +572,14 @@ where
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.prepare_items_if_needed(window, cx);
// Scroll to the selected item if it is set.
if let Some(ix) = self.deferred_scroll_to_index.take() {
if let Some(item_ix) = self.rows_cache.position_of(&ix) {
self.scroll_handle
.scroll_to_item(item_ix, ScrollStrategy::Top);
}
}
let items_count = self.rows_cache.items_count();
let entities_count = self.rows_cache.len();
let loading = self.delegate.loading(cx);

View file

@ -19,9 +19,10 @@ use std::{
use gpui::{
div, point, px, size, Along, AnyElement, App, AvailableSpace, Axis, Bounds, ContentMask,
Context, Div, Element, ElementId, Entity, GlobalElementId, Half, Hitbox, InteractiveElement,
IntoElement, IsZero as _, ListSizingBehavior, Pixels, Point, Render, ScrollHandle,
ScrollStrategy, Size, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window,
Context, DeferredScrollToItem, Div, Element, ElementId, Entity, GlobalElementId, Half, Hitbox,
InteractiveElement, IntoElement, IsZero as _, ListSizingBehavior, Pixels, Point, Render,
ScrollHandle, ScrollStrategy, Size, Stateful, StatefulInteractiveElement, StyleRefinement,
Styled, Window,
};
use smallvec::SmallVec;
@ -29,7 +30,8 @@ use crate::{scroll::ScrollHandleOffsetable, AxisExt};
struct VirtualListScrollHandleState {
axis: Axis,
items_bounds: Vec<Bounds<Pixels>>,
items_count: usize,
pub deferred_scroll_to_item: Option<DeferredScrollToItem>,
}
#[derive(Clone)]
@ -79,7 +81,8 @@ impl VirtualListScrollHandle {
VirtualListScrollHandle {
state: Rc::new(RefCell::new(VirtualListScrollHandleState {
axis: Axis::Vertical,
items_bounds: vec![],
items_count: 0,
deferred_scroll_to_item: None,
})),
base_handle: ScrollHandle::default(),
}
@ -89,65 +92,25 @@ impl VirtualListScrollHandle {
&self.base_handle
}
fn set_items_bounds(&self, items_bounds: Vec<Bounds<Pixels>>) {
self.state.borrow_mut().items_bounds = items_bounds;
}
fn set_axis(&self, axis: Axis) {
self.state.borrow_mut().axis = axis;
}
/// Scroll to the item at the given index.
pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
let state = self.state.borrow();
let Some(bounds) = state.items_bounds.get(ix) else {
return;
};
self.scroll_to_item_with_offset(ix, strategy, 0);
}
let axis = state.axis;
let mut scroll_offset = self.base_handle.offset();
let container_bounds = self.base_handle().bounds();
match strategy {
ScrollStrategy::Center => {
if axis.is_vertical() {
scroll_offset.y = container_bounds.top() + container_bounds.size.height.half()
- bounds.top()
- bounds.size.height.half()
} else {
scroll_offset.x = container_bounds.left() + container_bounds.size.width.half()
- bounds.left()
- bounds.size.width.half()
}
}
_ => {
// Ref: https://github.com/zed-industries/zed/blob/0d145289e0867a8d5d63e5e1397a5ca69c9d49c3/crates/gpui/src/elements/div.rs#L3026
if axis.is_vertical() {
if bounds.top() + scroll_offset.y < container_bounds.top() {
scroll_offset.y = container_bounds.top() - bounds.top();
} else if bounds.bottom() + scroll_offset.y > container_bounds.bottom() {
scroll_offset.y = container_bounds.bottom() - bounds.bottom();
}
} else {
if bounds.left() + scroll_offset.x < container_bounds.left() {
scroll_offset.x = container_bounds.left() - bounds.left();
} else if bounds.right() + scroll_offset.x > container_bounds.right() {
scroll_offset.x = container_bounds.right() - bounds.right();
}
}
}
}
self.base_handle.set_offset(scroll_offset);
/// Scroll to the item at the given index, with an additional offset items.
fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) {
let mut state = self.state.borrow_mut();
state.deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset,
});
}
/// Scrolls to the bottom of the list.
pub fn scroll_to_bottom(&self) {
let state = self.state.borrow();
self.scroll_to_item(
state.items_bounds.len().saturating_sub(1),
ScrollStrategy::Top,
);
let items_count = self.state.borrow().items_count;
self.scroll_to_item(items_count.saturating_sub(1), ScrollStrategy::Top);
}
}
@ -268,6 +231,54 @@ impl VirtualList {
self.scroll_handle = scroll_handle.clone();
self
}
fn scroll_to_deferred_item(
&self,
scroll_offset: Point<Pixels>,
items_bounds: &[Bounds<Pixels>],
content_bounds: &Bounds<Pixels>,
scroll_to_item: DeferredScrollToItem,
) -> Point<Pixels> {
let Some(bounds) = items_bounds
.get(scroll_to_item.item_index + scroll_to_item.offset)
.cloned()
else {
return scroll_offset;
};
let mut scroll_offset = scroll_offset;
match scroll_to_item.strategy {
ScrollStrategy::Center => {
if self.axis.is_vertical() {
scroll_offset.y = content_bounds.top() + content_bounds.size.height.half()
- bounds.top()
- bounds.size.height.half()
} else {
scroll_offset.x = content_bounds.left() + content_bounds.size.width.half()
- bounds.left()
- bounds.size.width.half()
}
}
_ => {
// Ref: https://github.com/zed-industries/zed/blob/0d145289e0867a8d5d63e5e1397a5ca69c9d49c3/crates/gpui/src/elements/div.rs#L3026
if self.axis.is_vertical() {
if bounds.top() + scroll_offset.y < content_bounds.top() {
scroll_offset.y = content_bounds.top() - bounds.top()
} else if bounds.bottom() + scroll_offset.y > content_bounds.bottom() {
scroll_offset.y = content_bounds.bottom() - bounds.bottom();
}
} else {
if bounds.left() + scroll_offset.x < content_bounds.left() {
scroll_offset.x = content_bounds.left() - bounds.left();
} else if bounds.right() + scroll_offset.x > content_bounds.right() {
scroll_offset.x = content_bounds.right() - bounds.right();
}
}
}
}
self.scroll_handle.set_offset(scroll_offset);
scroll_offset
}
}
/// Frame state used by the [VirtualItem].
@ -489,6 +500,19 @@ impl Element for VirtualList {
let item_sizes = &layout.size_layout.sizes;
let item_origins = &layout.size_layout.origins;
let content_bounds = Bounds::from_corners(
bounds.origin
+ point(
border_widths.left + paddings.left,
border_widths.top + paddings.top,
),
bounds.bottom_right()
- point(
border_widths.right + paddings.right,
border_widths.bottom + paddings.bottom,
),
);
// Update scroll_handle with the item bounds
let items_bounds = item_origins
.iter()
@ -498,18 +522,32 @@ impl Element for VirtualList {
Bounds {
origin: match self.axis {
Axis::Horizontal => point(bounds.left() + origin, px(0.)),
Axis::Vertical => point(px(0.), bounds.top() + origin),
Axis::Horizontal => point(content_bounds.left() + origin, px(0.)),
Axis::Vertical => point(px(0.), content_bounds.top() + origin),
},
size: match self.axis {
Axis::Horizontal => size(item_size, bounds.size.height),
Axis::Vertical => size(bounds.size.width, item_size),
Axis::Horizontal => size(item_size, content_bounds.size.height),
Axis::Vertical => size(content_bounds.size.width, item_size),
},
}
})
.collect::<Vec<_>>();
self.scroll_handle.set_axis(self.axis);
self.scroll_handle.set_items_bounds(items_bounds);
let axis = self.axis;
let mut scroll_state = self.scroll_handle.state.borrow_mut();
scroll_state.axis = axis;
scroll_state.items_count = self.items_count;
let mut scroll_offset = self.scroll_handle.offset();
if let Some(scroll_to_item) = scroll_state.deferred_scroll_to_item.take() {
scroll_offset = self.scroll_to_deferred_item(
scroll_offset,
&items_bounds,
&content_bounds,
scroll_to_item,
);
}
self.base.interactivity().prepaint(
global_id,
@ -519,24 +557,9 @@ impl Element for VirtualList {
window,
cx,
|_style, _, hitbox, window, cx| {
let mut scroll_offset = self.scroll_handle.offset();
let padded_bounds = Bounds::from_corners(
bounds.origin
+ point(
border_widths.left + paddings.left,
border_widths.top + paddings.top,
),
bounds.bottom_right()
- point(
border_widths.right + paddings.right,
border_widths.bottom + paddings.bottom,
),
);
if self.items_count > 0 {
let is_scrolled = !scroll_offset.along(self.axis).is_zero();
let min_scroll_offset = padded_bounds.size.along(self.axis)
let min_scroll_offset = content_bounds.size.along(self.axis)
- layout.size_layout.content_size.along(self.axis);
if is_scrolled {
@ -567,7 +590,8 @@ impl Element for VirtualList {
let mut last_visible_element_ix = 0;
for (i, &size) in item_sizes.iter().enumerate() {
cumulative_size += size;
if cumulative_size > (-scroll_offset.x + padded_bounds.size.width) {
if cumulative_size > (-scroll_offset.x + content_bounds.size.width)
{
last_visible_element_ix = i + 1;
break;
}
@ -594,7 +618,7 @@ impl Element for VirtualList {
let mut last_visible_element_ix = 0;
for (i, &size) in item_sizes.iter().enumerate() {
cumulative_size += size;
if cumulative_size > (-scroll_offset.y + padded_bounds.size.height)
if cumulative_size > (-scroll_offset.y + content_bounds.size.height)
{
last_visible_element_ix = i + 1;
break;
@ -619,11 +643,11 @@ impl Element for VirtualList {
for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
let item_origin = match self.axis {
Axis::Horizontal => {
padded_bounds.origin
content_bounds.origin
+ point(item_origins[ix] + scroll_offset.x, scroll_offset.y)
}
Axis::Vertical => {
padded_bounds.origin
content_bounds.origin
+ point(scroll_offset.x, item_origins[ix] + scroll_offset.y)
}
};
@ -631,10 +655,10 @@ impl Element for VirtualList {
let available_space = match self.axis {
Axis::Horizontal => size(
AvailableSpace::Definite(item_sizes[ix]),
AvailableSpace::Definite(padded_bounds.size.height),
AvailableSpace::Definite(content_bounds.size.height),
),
Axis::Vertical => size(
AvailableSpace::Definite(padded_bounds.size.width),
AvailableSpace::Definite(content_bounds.size.width),
AvailableSpace::Definite(item_sizes[ix]),
),
};