Return correct scroll top from layout when list is not full
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
@@ -28,14 +28,14 @@ struct StateInner {
|
||||
render_item: Box<dyn FnMut(usize, &mut LayoutContext) -> ElementBox>,
|
||||
rendered_range: Range<usize>,
|
||||
items: SumTree<ListItem>,
|
||||
scroll_top: Option<ScrollTop>,
|
||||
logical_scroll_top: Option<ListOffset>,
|
||||
orientation: Orientation,
|
||||
overdraw: f32,
|
||||
scroll_handler: Option<Box<dyn FnMut(Range<usize>, &mut EventContext)>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct ScrollTop {
|
||||
pub struct ListOffset {
|
||||
item_ix: usize,
|
||||
offset_in_item: f32,
|
||||
}
|
||||
@@ -84,7 +84,7 @@ impl List {
|
||||
}
|
||||
|
||||
impl Element for List {
|
||||
type LayoutState = ScrollTop;
|
||||
type LayoutState = ListOffset;
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
@@ -110,7 +110,7 @@ impl Element for List {
|
||||
let old_rendered_range = state.rendered_range.clone();
|
||||
let old_items = state.items.clone();
|
||||
let orientation = state.orientation;
|
||||
let mut stored_scroll_top = state.scroll_top;
|
||||
let mut stored_scroll_top = state.logical_scroll_top;
|
||||
let mut new_items = SumTree::new();
|
||||
|
||||
let mut render_item = |ix, old_item: &ListItem| {
|
||||
@@ -146,7 +146,7 @@ impl Element for List {
|
||||
cursor.prev(&());
|
||||
}
|
||||
}
|
||||
scroll_top = ScrollTop {
|
||||
scroll_top = ListOffset {
|
||||
item_ix: cursor.seek_start().0,
|
||||
offset_in_item: rendered_height - size.y(),
|
||||
};
|
||||
@@ -190,10 +190,11 @@ impl Element for List {
|
||||
|
||||
if adjust_scroll_top && rendered_height >= size.y() {
|
||||
let offset_in_item = rendered_height - size.y();
|
||||
stored_scroll_top = Some(ScrollTop {
|
||||
scroll_top = ListOffset {
|
||||
item_ix: new_rendered_start_ix,
|
||||
offset_in_item,
|
||||
});
|
||||
};
|
||||
stored_scroll_top = Some(scroll_top);
|
||||
remaining_overdraw = overdraw - offset_in_item;
|
||||
adjust_scroll_top = false;
|
||||
}
|
||||
@@ -204,10 +205,11 @@ impl Element for List {
|
||||
|
||||
if adjust_scroll_top && next_rendered_height >= size.y() {
|
||||
let offset_in_item = next_rendered_height - size.y();
|
||||
stored_scroll_top = Some(ScrollTop {
|
||||
scroll_top = ListOffset {
|
||||
item_ix: new_rendered_start_ix - 1,
|
||||
offset_in_item,
|
||||
});
|
||||
};
|
||||
stored_scroll_top = Some(scroll_top);
|
||||
remaining_overdraw = overdraw + (element.size().y() - offset_in_item);
|
||||
adjust_scroll_top = false;
|
||||
}
|
||||
@@ -230,7 +232,7 @@ impl Element for List {
|
||||
stored_scroll_top = None;
|
||||
scroll_top = match orientation {
|
||||
Orientation::Top => Default::default(),
|
||||
Orientation::Bottom => ScrollTop {
|
||||
Orientation::Bottom => ListOffset {
|
||||
item_ix: cursor.seek_start().0,
|
||||
offset_in_item: rendered_height - size.y(),
|
||||
},
|
||||
@@ -280,11 +282,11 @@ impl Element for List {
|
||||
state.items = new_items;
|
||||
state.rendered_range = new_rendered_range;
|
||||
state.last_layout_width = Some(size.x());
|
||||
state.scroll_top = stored_scroll_top;
|
||||
state.logical_scroll_top = stored_scroll_top;
|
||||
(size, scroll_top)
|
||||
}
|
||||
|
||||
fn paint(&mut self, bounds: RectF, scroll_top: &mut ScrollTop, cx: &mut PaintContext) {
|
||||
fn paint(&mut self, bounds: RectF, scroll_top: &mut ListOffset, cx: &mut PaintContext) {
|
||||
cx.scene.push_layer(Some(bounds));
|
||||
|
||||
let state = &mut *self.state.0.borrow_mut();
|
||||
@@ -299,7 +301,7 @@ impl Element for List {
|
||||
&mut self,
|
||||
event: &Event,
|
||||
bounds: RectF,
|
||||
scroll_top: &mut ScrollTop,
|
||||
scroll_top: &mut ListOffset,
|
||||
_: &mut (),
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
@@ -344,7 +346,7 @@ impl Element for List {
|
||||
json!({
|
||||
"visible_range": visible_range,
|
||||
"visible_elements": visible_elements,
|
||||
"scroll_top": state.scroll_top.map(|top| (top.item_ix, top.offset_in_item)),
|
||||
"scroll_top": state.logical_scroll_top.map(|top| (top.item_ix, top.offset_in_item)),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -366,7 +368,7 @@ impl ListState {
|
||||
render_item: Box::new(render_item),
|
||||
rendered_range: 0..0,
|
||||
items,
|
||||
scroll_top: None,
|
||||
logical_scroll_top: None,
|
||||
orientation,
|
||||
overdraw,
|
||||
scroll_handler: None,
|
||||
@@ -375,7 +377,7 @@ impl ListState {
|
||||
|
||||
pub fn reset(&self, element_count: usize) {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
state.scroll_top = None;
|
||||
state.logical_scroll_top = None;
|
||||
state.items = SumTree::new();
|
||||
state
|
||||
.items
|
||||
@@ -385,10 +387,10 @@ impl ListState {
|
||||
pub fn splice(&self, old_range: Range<usize>, count: usize) {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
|
||||
if let Some(ScrollTop {
|
||||
if let Some(ListOffset {
|
||||
item_ix,
|
||||
offset_in_item,
|
||||
}) = state.scroll_top.as_mut()
|
||||
}) = state.logical_scroll_top.as_mut()
|
||||
{
|
||||
if old_range.contains(item_ix) {
|
||||
*item_ix = old_range.start;
|
||||
@@ -427,7 +429,7 @@ impl ListState {
|
||||
}
|
||||
|
||||
impl StateInner {
|
||||
fn visible_range(&self, height: f32, scroll_top: &ScrollTop) -> Range<usize> {
|
||||
fn visible_range(&self, height: f32, scroll_top: &ListOffset) -> Range<usize> {
|
||||
let mut cursor = self.items.cursor::<Count, Height>();
|
||||
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
|
||||
let start_y = cursor.sum_start().0 + scroll_top.offset_in_item;
|
||||
@@ -439,7 +441,7 @@ impl StateInner {
|
||||
fn visible_elements<'a>(
|
||||
&'a self,
|
||||
bounds: RectF,
|
||||
scroll_top: &ScrollTop,
|
||||
scroll_top: &ListOffset,
|
||||
) -> impl Iterator<Item = (ElementRc, Vector2F)> + 'a {
|
||||
let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
|
||||
let mut cursor = self.items.cursor::<Count, ()>();
|
||||
@@ -466,7 +468,7 @@ impl StateInner {
|
||||
|
||||
fn scroll(
|
||||
&mut self,
|
||||
scroll_top: &ScrollTop,
|
||||
scroll_top: &ListOffset,
|
||||
height: f32,
|
||||
mut delta: Vector2F,
|
||||
precise: bool,
|
||||
@@ -477,18 +479,18 @@ impl StateInner {
|
||||
}
|
||||
|
||||
let scroll_max = (self.items.summary().height - height).max(0.);
|
||||
let new_scroll_top = (self.scroll_top(height) + delta.y())
|
||||
let new_scroll_top = (self.scroll_top(scroll_top) + delta.y())
|
||||
.max(0.)
|
||||
.min(scroll_max);
|
||||
|
||||
if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max {
|
||||
self.scroll_top = None;
|
||||
self.logical_scroll_top = None;
|
||||
} else {
|
||||
let mut cursor = self.items.cursor::<Height, Count>();
|
||||
cursor.seek(&Height(new_scroll_top), Bias::Right, &());
|
||||
let item_ix = cursor.sum_start().0;
|
||||
let offset_in_item = new_scroll_top - cursor.seek_start().0;
|
||||
self.scroll_top = Some(ScrollTop {
|
||||
self.logical_scroll_top = Some(ListOffset {
|
||||
item_ix,
|
||||
offset_in_item,
|
||||
});
|
||||
@@ -503,24 +505,10 @@ impl StateInner {
|
||||
true
|
||||
}
|
||||
|
||||
fn scroll_top(&self, height: f32) -> f32 {
|
||||
let scroll_max = (self.items.summary().height - height).max(0.);
|
||||
if let Some(ScrollTop {
|
||||
item_ix,
|
||||
offset_in_item,
|
||||
}) = self.scroll_top
|
||||
{
|
||||
let mut cursor = self.items.cursor::<Count, Height>();
|
||||
cursor.seek(&Count(item_ix), Bias::Right, &());
|
||||
let scroll_top = cursor.sum_start().0 + offset_in_item;
|
||||
assert!(scroll_top <= scroll_max);
|
||||
scroll_top
|
||||
} else {
|
||||
match self.orientation {
|
||||
Orientation::Top => 0.,
|
||||
Orientation::Bottom => scroll_max,
|
||||
}
|
||||
}
|
||||
fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
|
||||
let mut cursor = self.items.cursor::<Count, Height>();
|
||||
cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
|
||||
cursor.sum_start().0 + logical_scroll_top.offset_in_item
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,6 +606,7 @@ mod tests {
|
||||
#[crate::test(self)]
|
||||
fn test_layout(cx: &mut crate::MutableAppContext) {
|
||||
let mut presenter = cx.build_presenter(0, 0.);
|
||||
let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
|
||||
|
||||
let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)]));
|
||||
let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, {
|
||||
@@ -628,11 +617,8 @@ mod tests {
|
||||
}
|
||||
});
|
||||
|
||||
let mut list = List::new(state.clone()).boxed();
|
||||
let size = list.layout(
|
||||
SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)),
|
||||
&mut presenter.build_layout_context(cx),
|
||||
);
|
||||
let mut list = List::new(state.clone());
|
||||
let (size, _) = list.layout(constraint, &mut presenter.build_layout_context(cx));
|
||||
assert_eq!(size, vec2f(100., 40.));
|
||||
assert_eq!(
|
||||
state.0.borrow().items.summary(),
|
||||
@@ -645,7 +631,7 @@ mod tests {
|
||||
);
|
||||
|
||||
state.0.borrow_mut().scroll(
|
||||
&ScrollTop {
|
||||
&ListOffset {
|
||||
item_ix: 0,
|
||||
offset_in_item: 0.,
|
||||
},
|
||||
@@ -654,14 +640,16 @@ mod tests {
|
||||
true,
|
||||
&mut presenter.build_event_context(cx),
|
||||
);
|
||||
let (_, logical_scroll_top) =
|
||||
list.layout(constraint, &mut presenter.build_layout_context(cx));
|
||||
assert_eq!(
|
||||
state.0.borrow().scroll_top,
|
||||
Some(ScrollTop {
|
||||
logical_scroll_top,
|
||||
ListOffset {
|
||||
item_ix: 2,
|
||||
offset_in_item: 4.
|
||||
})
|
||||
}
|
||||
);
|
||||
assert_eq!(state.0.borrow().scroll_top(size.y()), 54.);
|
||||
assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 54.);
|
||||
|
||||
elements.borrow_mut().splice(1..2, vec![(3, 40.), (4, 50.)]);
|
||||
elements.borrow_mut().push((5, 60.));
|
||||
@@ -677,11 +665,8 @@ mod tests {
|
||||
}
|
||||
);
|
||||
|
||||
let mut list = List::new(state.clone()).boxed();
|
||||
let size = list.layout(
|
||||
SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)),
|
||||
&mut presenter.build_layout_context(cx),
|
||||
);
|
||||
let (size, logical_scroll_top) =
|
||||
list.layout(constraint, &mut presenter.build_layout_context(cx));
|
||||
assert_eq!(size, vec2f(100., 40.));
|
||||
assert_eq!(
|
||||
state.0.borrow().items.summary(),
|
||||
@@ -693,13 +678,13 @@ mod tests {
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
state.0.borrow().scroll_top,
|
||||
Some(ScrollTop {
|
||||
logical_scroll_top,
|
||||
ListOffset {
|
||||
item_ix: 3,
|
||||
offset_in_item: 4.
|
||||
})
|
||||
}
|
||||
);
|
||||
assert_eq!(state.0.borrow().scroll_top(size.y()), 114.);
|
||||
assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.);
|
||||
}
|
||||
|
||||
#[crate::test(self, iterations = 10000, seed = 0)]
|
||||
@@ -739,18 +724,18 @@ mod tests {
|
||||
log::info!("size: ({:?}, {:?})", width, height);
|
||||
log::info!("==================");
|
||||
|
||||
let mut scroll_top = None;
|
||||
let mut last_logical_scroll_top = None;
|
||||
for _ in 0..operations {
|
||||
match rng.gen_range(0..=100) {
|
||||
0..=29 if scroll_top.is_some() => {
|
||||
0..=29 if last_logical_scroll_top.is_some() => {
|
||||
let delta = vec2f(0., rng.gen_range(-overdraw..=overdraw));
|
||||
log::info!(
|
||||
"Scrolling by {:?}, previous scroll top: {:?}",
|
||||
delta,
|
||||
scroll_top.unwrap()
|
||||
last_logical_scroll_top.unwrap()
|
||||
);
|
||||
state.0.borrow_mut().scroll(
|
||||
scroll_top.as_ref().unwrap(),
|
||||
last_logical_scroll_top.as_ref().unwrap(),
|
||||
height,
|
||||
delta,
|
||||
true,
|
||||
@@ -791,26 +776,30 @@ mod tests {
|
||||
}
|
||||
|
||||
let mut list = List::new(state.clone());
|
||||
let (size, new_scroll_top) = list.layout(
|
||||
let (size, logical_scroll_top) = list.layout(
|
||||
SizeConstraint::new(vec2f(0., 0.), vec2f(width, height)),
|
||||
&mut presenter.build_layout_context(cx),
|
||||
);
|
||||
assert_eq!(size, vec2f(width, height));
|
||||
scroll_top = Some(new_scroll_top);
|
||||
last_logical_scroll_top = Some(logical_scroll_top);
|
||||
|
||||
let state = state.0.borrow();
|
||||
log::info!("items {:?}", state.items.items(&()));
|
||||
|
||||
let rendered_top = (state.scroll_top(height) - overdraw).max(0.);
|
||||
let rendered_bottom = state.scroll_top(height) + height + overdraw;
|
||||
let scroll_top = state.scroll_top(&logical_scroll_top);
|
||||
let rendered_top = (scroll_top - overdraw).max(0.);
|
||||
let rendered_bottom = scroll_top + height + overdraw;
|
||||
let mut item_top = 0.;
|
||||
|
||||
log::info!(
|
||||
"rendered top {:?}, rendered bottom {:?}",
|
||||
"rendered top {:?}, rendered bottom {:?}, scroll top {:?}",
|
||||
rendered_top,
|
||||
rendered_bottom
|
||||
rendered_bottom,
|
||||
scroll_top,
|
||||
);
|
||||
|
||||
let mut first_rendered_element_top = None;
|
||||
let mut last_rendered_element_bottom = None;
|
||||
assert_eq!(state.items.summary().count, elements.borrow().len());
|
||||
for (ix, item) in state.items.cursor::<(), ()>().enumerate() {
|
||||
match item {
|
||||
@@ -837,11 +826,26 @@ mod tests {
|
||||
});
|
||||
assert_eq!(element.size().y(), expected_height);
|
||||
let item_bottom = item_top + element.size().y();
|
||||
first_rendered_element_top.get_or_insert(item_top);
|
||||
last_rendered_element_bottom = Some(item_bottom);
|
||||
assert!(item_bottom > rendered_top || item_top < rendered_bottom);
|
||||
item_top = item_bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match orientation {
|
||||
Orientation::Top => {
|
||||
if let Some(first_rendered_element_top) = first_rendered_element_top {
|
||||
assert!(first_rendered_element_top <= scroll_top);
|
||||
}
|
||||
}
|
||||
Orientation::Bottom => {
|
||||
if let Some(last_rendered_element_bottom) = last_rendered_element_bottom {
|
||||
assert!(last_rendered_element_bottom >= scroll_top + height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user