This reduces the size of cursor by a usize when the summary does not
require a context making Cursor usages and constructions slightly more
efficient.
This change is a bit annoying though, as Rust has no means of
specializing, so this uses a `ContextlessSummary` trait with a blanket
impl while turning the `Context` into a GAT `Context<'a>`. This means
`Summary` implies are a bit more verbose now while contextless ones are
slimmer. It does come with the downside that the lifetime in the GAT is
always considered invariant, so some lifetime splitting occurred due to
that.
```
push/4096 time: [352.65 µs 360.87 µs 367.80 µs]
thrpt: [10.621 MiB/s 10.825 MiB/s 11.077 MiB/s]
change:
time: [-2.6633% -1.3640% -0.0561%] (p = 0.05 < 0.05)
thrpt: [+0.0561% +1.3828% +2.7361%]
Change within noise threshold.
Found 16 outliers among 100 measurements (16.00%)
7 (7.00%) low severe
3 (3.00%) low mild
2 (2.00%) high mild
4 (4.00%) high severe
push/65536 time: [1.2917 ms 1.2949 ms 1.2979 ms]
thrpt: [48.156 MiB/s 48.267 MiB/s 48.387 MiB/s]
change:
time: [+1.4428% +1.9844% +2.5299%] (p = 0.00 < 0.05)
thrpt: [-2.4675% -1.9458% -1.4223%]
Performance has regressed.
Found 3 outliers among 100 measurements (3.00%)
1 (1.00%) low severe
1 (1.00%) low mild
1 (1.00%) high severe
append/4096 time: [677.87 ns 678.87 ns 679.83 ns]
thrpt: [5.6112 GiB/s 5.6192 GiB/s 5.6274 GiB/s]
change:
time: [-0.8924% -0.5017% -0.1705%] (p = 0.00 < 0.05)
thrpt: [+0.1708% +0.5043% +0.9004%]
Change within noise threshold.
Found 2 outliers among 100 measurements (2.00%)
1 (1.00%) low mild
1 (1.00%) high mild
append/65536 time: [9.3275 µs 9.3406 µs 9.3536 µs]
thrpt: [6.5253 GiB/s 6.5344 GiB/s 6.5435 GiB/s]
change:
time: [+0.5409% +0.7215% +0.9054%] (p = 0.00 < 0.05)
thrpt: [-0.8973% -0.7163% -0.5380%]
Change within noise threshold.
slice/4096 time: [27.673 µs 27.791 µs 27.907 µs]
thrpt: [139.97 MiB/s 140.56 MiB/s 141.16 MiB/s]
change:
time: [-1.1065% -0.6725% -0.2429%] (p = 0.00 < 0.05)
thrpt: [+0.2435% +0.6770% +1.1189%]
Change within noise threshold.
Found 5 outliers among 100 measurements (5.00%)
4 (4.00%) low mild
1 (1.00%) high mild
slice/65536 time: [507.55 µs 517.40 µs 535.60 µs]
thrpt: [116.69 MiB/s 120.80 MiB/s 123.14 MiB/s]
change:
time: [-1.3489% +0.0599% +2.2591%] (p = 0.96 > 0.05)
thrpt: [-2.2092% -0.0598% +1.3674%]
No change in performance detected.
Found 8 outliers among 100 measurements (8.00%)
5 (5.00%) low mild
2 (2.00%) high mild
1 (1.00%) high severe
bytes_in_range/4096 time: [3.3917 µs 3.4108 µs 3.4313 µs]
thrpt: [1.1117 GiB/s 1.1184 GiB/s 1.1247 GiB/s]
change:
time: [-5.3466% -4.7193% -4.1262%] (p = 0.00 < 0.05)
thrpt: [+4.3038% +4.9531% +5.6487%]
Performance has improved.
Found 6 outliers among 100 measurements (6.00%)
1 (1.00%) low mild
5 (5.00%) high mild
bytes_in_range/65536 time: [88.175 µs 88.613 µs 89.111 µs]
thrpt: [701.37 MiB/s 705.31 MiB/s 708.82 MiB/s]
change:
time: [-0.6935% +0.3769% +1.4655%] (p = 0.50 > 0.05)
thrpt: [-1.4443% -0.3755% +0.6984%]
No change in performance detected.
Found 2 outliers among 100 measurements (2.00%)
2 (2.00%) high mild
chars/4096 time: [678.70 ns 680.38 ns 682.08 ns]
thrpt: [5.5927 GiB/s 5.6067 GiB/s 5.6206 GiB/s]
change:
time: [-0.6969% -0.2755% +0.1485%] (p = 0.20 > 0.05)
thrpt: [-0.1483% +0.2763% +0.7018%]
No change in performance detected.
Found 9 outliers among 100 measurements (9.00%)
5 (5.00%) low mild
4 (4.00%) high mild
chars/65536 time: [12.720 µs 12.775 µs 12.830 µs]
thrpt: [4.7573 GiB/s 4.7778 GiB/s 4.7983 GiB/s]
change:
time: [-0.6172% -0.1110% +0.4179%] (p = 0.68 > 0.05)
thrpt: [-0.4162% +0.1112% +0.6211%]
No change in performance detected.
Found 2 outliers among 100 measurements (2.00%)
1 (1.00%) low mild
1 (1.00%) high mild
clip_point/4096 time: [33.240 µs 33.310 µs 33.394 µs]
thrpt: [116.98 MiB/s 117.27 MiB/s 117.52 MiB/s]
change:
time: [-2.8892% -2.6305% -2.3438%] (p = 0.00 < 0.05)
thrpt: [+2.4000% +2.7015% +2.9751%]
Performance has improved.
Found 12 outliers among 100 measurements (12.00%)
1 (1.00%) low mild
4 (4.00%) high mild
7 (7.00%) high severe
clip_point/65536 time: [1.6531 ms 1.6586 ms 1.6640 ms]
thrpt: [37.560 MiB/s 37.683 MiB/s 37.808 MiB/s]
change:
time: [-6.6381% -5.9395% -5.2680%] (p = 0.00 < 0.05)
thrpt: [+5.5610% +6.3146% +7.1100%]
Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
1 (1.00%) low mild
2 (2.00%) high mild
4 (4.00%) high severe
point_to_offset/4096 time: [11.586 µs 11.603 µs 11.621 µs]
thrpt: [336.15 MiB/s 336.67 MiB/s 337.16 MiB/s]
change:
time: [-14.289% -14.111% -13.939%] (p = 0.00 < 0.05)
thrpt: [+16.197% +16.429% +16.672%]
Performance has improved.
Found 12 outliers among 100 measurements (12.00%)
3 (3.00%) low severe
5 (5.00%) low mild
4 (4.00%) high mild
point_to_offset/65536 time: [527.74 µs 532.08 µs 536.51 µs]
thrpt: [116.49 MiB/s 117.46 MiB/s 118.43 MiB/s]
change:
time: [-6.7825% -4.6235% -2.3533%] (p = 0.00 < 0.05)
thrpt: [+2.4100% +4.8477% +7.2760%]
Performance has improved.
Found 8 outliers among 100 measurements (8.00%)
4 (4.00%) high mild
4 (4.00%) high severe
cursor/4096 time: [16.154 µs 16.192 µs 16.232 µs]
thrpt: [240.66 MiB/s 241.24 MiB/s 241.81 MiB/s]
change:
time: [-3.2536% -2.9145% -2.5526%] (p = 0.00 < 0.05)
thrpt: [+2.6194% +3.0019% +3.3630%]
Performance has improved.
Found 5 outliers among 100 measurements (5.00%)
1 (1.00%) low mild
2 (2.00%) high mild
2 (2.00%) high severe
cursor/65536 time: [509.60 µs 511.24 µs 512.93 µs]
thrpt: [121.85 MiB/s 122.25 MiB/s 122.65 MiB/s]
change:
time: [-7.3677% -6.6017% -5.7840%] (p = 0.00 < 0.05)
thrpt: [+6.1391% +7.0683% +7.9537%]
Performance has improved.
Found 6 outliers among 100 measurements (6.00%)
3 (3.00%) high mild
3 (3.00%) high severe
```
Release Notes:
- N/A *or* Added/Fixed/Improved ...
1203 lines
43 KiB
Rust
1203 lines
43 KiB
Rust
//! A list element that can be used to render a large number of differently sized elements
|
|
//! efficiently. Clients of this API need to ensure that elements outside of the scrolled
|
|
//! area do not change their height for this element to function correctly. If your elements
|
|
//! do change height, notify the list element via [`ListState::splice`] or [`ListState::reset`].
|
|
//! In order to minimize re-renders, this element's state is stored intrusively
|
|
//! on your own views, so that your code can coordinate directly with the list element's cached state.
|
|
//!
|
|
//! If all of your elements are the same height, see [`crate::UniformList`] for a simpler API
|
|
|
|
use crate::{
|
|
AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
|
|
FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,
|
|
Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
|
|
Window, point, px, size,
|
|
};
|
|
use collections::VecDeque;
|
|
use refineable::Refineable as _;
|
|
use std::{cell::RefCell, ops::Range, rc::Rc};
|
|
use sum_tree::{Bias, Dimensions, SumTree};
|
|
|
|
type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static;
|
|
|
|
/// Construct a new list element
|
|
pub fn list(
|
|
state: ListState,
|
|
render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static,
|
|
) -> List {
|
|
List {
|
|
state,
|
|
render_item: Box::new(render_item),
|
|
style: StyleRefinement::default(),
|
|
sizing_behavior: ListSizingBehavior::default(),
|
|
}
|
|
}
|
|
|
|
/// A list element
|
|
pub struct List {
|
|
state: ListState,
|
|
render_item: Box<RenderItemFn>,
|
|
style: StyleRefinement,
|
|
sizing_behavior: ListSizingBehavior,
|
|
}
|
|
|
|
impl List {
|
|
/// Set the sizing behavior for the list.
|
|
pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
|
|
self.sizing_behavior = behavior;
|
|
self
|
|
}
|
|
}
|
|
|
|
/// The list state that views must hold on behalf of the list element.
|
|
#[derive(Clone)]
|
|
pub struct ListState(Rc<RefCell<StateInner>>);
|
|
|
|
impl std::fmt::Debug for ListState {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str("ListState")
|
|
}
|
|
}
|
|
|
|
struct StateInner {
|
|
last_layout_bounds: Option<Bounds<Pixels>>,
|
|
last_padding: Option<Edges<Pixels>>,
|
|
items: SumTree<ListItem>,
|
|
logical_scroll_top: Option<ListOffset>,
|
|
alignment: ListAlignment,
|
|
overdraw: Pixels,
|
|
reset: bool,
|
|
#[allow(clippy::type_complexity)]
|
|
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,
|
|
scrollbar_drag_start_height: Option<Pixels>,
|
|
}
|
|
|
|
/// Whether the list is scrolling from top to bottom or bottom to top.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ListAlignment {
|
|
/// The list is scrolling from top to bottom, like most lists.
|
|
Top,
|
|
/// The list is scrolling from bottom to top, like a chat log.
|
|
Bottom,
|
|
}
|
|
|
|
/// A scroll event that has been converted to be in terms of the list's items.
|
|
pub struct ListScrollEvent {
|
|
/// The range of items currently visible in the list, after applying the scroll event.
|
|
pub visible_range: Range<usize>,
|
|
|
|
/// The number of items that are currently visible in the list, after applying the scroll event.
|
|
pub count: usize,
|
|
|
|
/// Whether the list has been scrolled.
|
|
pub is_scrolled: bool,
|
|
}
|
|
|
|
/// The sizing behavior to apply during layout.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
pub enum ListSizingBehavior {
|
|
/// The list should calculate its size based on the size of its items.
|
|
Infer,
|
|
/// The list should not calculate a fixed size.
|
|
#[default]
|
|
Auto,
|
|
}
|
|
|
|
/// The horizontal sizing behavior to apply during layout.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
pub enum ListHorizontalSizingBehavior {
|
|
/// List items' width can never exceed the width of the list.
|
|
#[default]
|
|
FitList,
|
|
/// List items' width may go over the width of the list, if any item is wider.
|
|
Unconstrained,
|
|
}
|
|
|
|
struct LayoutItemsResponse {
|
|
max_item_width: Pixels,
|
|
scroll_top: ListOffset,
|
|
item_layouts: VecDeque<ItemLayout>,
|
|
}
|
|
|
|
struct ItemLayout {
|
|
index: usize,
|
|
element: AnyElement,
|
|
size: Size<Pixels>,
|
|
}
|
|
|
|
/// Frame state used by the [List] element after layout.
|
|
pub struct ListPrepaintState {
|
|
hitbox: Hitbox,
|
|
layout: LayoutItemsResponse,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum ListItem {
|
|
Unmeasured {
|
|
focus_handle: Option<FocusHandle>,
|
|
},
|
|
Measured {
|
|
size: Size<Pixels>,
|
|
focus_handle: Option<FocusHandle>,
|
|
},
|
|
}
|
|
|
|
impl ListItem {
|
|
fn size(&self) -> Option<Size<Pixels>> {
|
|
if let ListItem::Measured { size, .. } = self {
|
|
Some(*size)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn focus_handle(&self) -> Option<FocusHandle> {
|
|
match self {
|
|
ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
|
|
focus_handle.clone()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn contains_focused(&self, window: &Window, cx: &App) -> bool {
|
|
match self {
|
|
ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
|
|
focus_handle
|
|
.as_ref()
|
|
.is_some_and(|handle| handle.contains_focused(window, cx))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq)]
|
|
struct ListItemSummary {
|
|
count: usize,
|
|
rendered_count: usize,
|
|
unrendered_count: usize,
|
|
height: Pixels,
|
|
has_focus_handles: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
|
struct Count(usize);
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
struct Height(Pixels);
|
|
|
|
impl ListState {
|
|
/// Construct a new list state, for storage on a view.
|
|
///
|
|
/// The overdraw parameter controls how much extra space is rendered
|
|
/// above and below the visible area. Elements within this area will
|
|
/// be measured even though they are not visible. This can help ensure
|
|
/// that the list doesn't flicker or pop in when scrolling.
|
|
pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self {
|
|
let this = Self(Rc::new(RefCell::new(StateInner {
|
|
last_layout_bounds: None,
|
|
last_padding: None,
|
|
items: SumTree::default(),
|
|
logical_scroll_top: None,
|
|
alignment,
|
|
overdraw,
|
|
scroll_handler: None,
|
|
reset: false,
|
|
scrollbar_drag_start_height: None,
|
|
})));
|
|
this.splice(0..0, item_count);
|
|
this
|
|
}
|
|
|
|
/// Reset this instantiation of the list state.
|
|
///
|
|
/// Note that this will cause scroll events to be dropped until the next paint.
|
|
pub fn reset(&self, element_count: usize) {
|
|
let old_count = {
|
|
let state = &mut *self.0.borrow_mut();
|
|
state.reset = true;
|
|
state.logical_scroll_top = None;
|
|
state.scrollbar_drag_start_height = None;
|
|
state.items.summary().count
|
|
};
|
|
|
|
self.splice(0..old_count, element_count);
|
|
}
|
|
|
|
/// The number of items in this list.
|
|
pub fn item_count(&self) -> usize {
|
|
self.0.borrow().items.summary().count
|
|
}
|
|
|
|
/// Inform the list state that the items in `old_range` have been replaced
|
|
/// by `count` new items that must be recalculated.
|
|
pub fn splice(&self, old_range: Range<usize>, count: usize) {
|
|
self.splice_focusable(old_range, (0..count).map(|_| None))
|
|
}
|
|
|
|
/// Register with the list state that the items in `old_range` have been replaced
|
|
/// by new items. As opposed to [`Self::splice`], this method allows an iterator of optional focus handles
|
|
/// to be supplied to properly integrate with items in the list that can be focused. If a focused item
|
|
/// is scrolled out of view, the list will continue to render it to allow keyboard interaction.
|
|
pub fn splice_focusable(
|
|
&self,
|
|
old_range: Range<usize>,
|
|
focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
|
|
) {
|
|
let state = &mut *self.0.borrow_mut();
|
|
|
|
let mut old_items = state.items.cursor::<Count>(());
|
|
let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right);
|
|
old_items.seek_forward(&Count(old_range.end), Bias::Right);
|
|
|
|
let mut spliced_count = 0;
|
|
new_items.extend(
|
|
focus_handles.into_iter().map(|focus_handle| {
|
|
spliced_count += 1;
|
|
ListItem::Unmeasured { focus_handle }
|
|
}),
|
|
(),
|
|
);
|
|
new_items.append(old_items.suffix(), ());
|
|
drop(old_items);
|
|
state.items = new_items;
|
|
|
|
if let Some(ListOffset {
|
|
item_ix,
|
|
offset_in_item,
|
|
}) = state.logical_scroll_top.as_mut()
|
|
{
|
|
if old_range.contains(item_ix) {
|
|
*item_ix = old_range.start;
|
|
*offset_in_item = px(0.);
|
|
} else if old_range.end <= *item_ix {
|
|
*item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Set a handler that will be called when the list is scrolled.
|
|
pub fn set_scroll_handler(
|
|
&self,
|
|
handler: impl FnMut(&ListScrollEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
|
|
}
|
|
|
|
/// Get the current scroll offset, in terms of the list's items.
|
|
pub fn logical_scroll_top(&self) -> ListOffset {
|
|
self.0.borrow().logical_scroll_top()
|
|
}
|
|
|
|
/// Scroll the list by the given offset
|
|
pub fn scroll_by(&self, distance: Pixels) {
|
|
if distance == px(0.) {
|
|
return;
|
|
}
|
|
|
|
let current_offset = self.logical_scroll_top();
|
|
let state = &mut *self.0.borrow_mut();
|
|
let mut cursor = state.items.cursor::<ListItemSummary>(());
|
|
cursor.seek(&Count(current_offset.item_ix), Bias::Right);
|
|
|
|
let start_pixel_offset = cursor.start().height + current_offset.offset_in_item;
|
|
let new_pixel_offset = (start_pixel_offset + distance).max(px(0.));
|
|
if new_pixel_offset > start_pixel_offset {
|
|
cursor.seek_forward(&Height(new_pixel_offset), Bias::Right);
|
|
} else {
|
|
cursor.seek(&Height(new_pixel_offset), Bias::Right);
|
|
}
|
|
|
|
state.logical_scroll_top = Some(ListOffset {
|
|
item_ix: cursor.start().count,
|
|
offset_in_item: new_pixel_offset - cursor.start().height,
|
|
});
|
|
}
|
|
|
|
/// Scroll the list to the given offset
|
|
pub fn scroll_to(&self, mut scroll_top: ListOffset) {
|
|
let state = &mut *self.0.borrow_mut();
|
|
let item_count = state.items.summary().count;
|
|
if scroll_top.item_ix >= item_count {
|
|
scroll_top.item_ix = item_count;
|
|
scroll_top.offset_in_item = px(0.);
|
|
}
|
|
|
|
state.logical_scroll_top = Some(scroll_top);
|
|
}
|
|
|
|
/// Scroll the list to the given item, such that the item is fully visible.
|
|
pub fn scroll_to_reveal_item(&self, ix: usize) {
|
|
let state = &mut *self.0.borrow_mut();
|
|
|
|
let mut scroll_top = state.logical_scroll_top();
|
|
let height = state
|
|
.last_layout_bounds
|
|
.map_or(px(0.), |bounds| bounds.size.height);
|
|
let padding = state.last_padding.unwrap_or_default();
|
|
|
|
if ix <= scroll_top.item_ix {
|
|
scroll_top.item_ix = ix;
|
|
scroll_top.offset_in_item = px(0.);
|
|
} else {
|
|
let mut cursor = state.items.cursor::<ListItemSummary>(());
|
|
cursor.seek(&Count(ix + 1), Bias::Right);
|
|
let bottom = cursor.start().height + padding.top;
|
|
let goal_top = px(0.).max(bottom - height + padding.bottom);
|
|
|
|
cursor.seek(&Height(goal_top), Bias::Left);
|
|
let start_ix = cursor.start().count;
|
|
let start_item_top = cursor.start().height;
|
|
|
|
if start_ix >= scroll_top.item_ix {
|
|
scroll_top.item_ix = start_ix;
|
|
scroll_top.offset_in_item = goal_top - start_item_top;
|
|
}
|
|
}
|
|
|
|
state.logical_scroll_top = Some(scroll_top);
|
|
}
|
|
|
|
/// Get the bounds for the given item in window coordinates, if it's
|
|
/// been rendered.
|
|
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
|
|
let state = &*self.0.borrow();
|
|
|
|
let bounds = state.last_layout_bounds.unwrap_or_default();
|
|
let scroll_top = state.logical_scroll_top();
|
|
if ix < scroll_top.item_ix {
|
|
return None;
|
|
}
|
|
|
|
let mut cursor = state.items.cursor::<Dimensions<Count, Height>>(());
|
|
cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
|
|
|
|
let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item;
|
|
|
|
cursor.seek_forward(&Count(ix), Bias::Right);
|
|
if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
|
|
let &Dimensions(Count(count), Height(top), _) = cursor.start();
|
|
if count == ix {
|
|
let top = bounds.top() + top - scroll_top;
|
|
return Some(Bounds::from_corners(
|
|
point(bounds.left(), top),
|
|
point(bounds.right(), top + size.height),
|
|
));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Call this method when the user starts dragging the scrollbar.
|
|
///
|
|
/// This will prevent the height reported to the scrollbar from changing during the drag
|
|
/// as items in the overdraw get measured, and help offset scroll position changes accordingly.
|
|
pub fn scrollbar_drag_started(&self) {
|
|
let mut state = self.0.borrow_mut();
|
|
state.scrollbar_drag_start_height = Some(state.items.summary().height);
|
|
}
|
|
|
|
/// Called when the user stops dragging the scrollbar.
|
|
///
|
|
/// See `scrollbar_drag_started`.
|
|
pub fn scrollbar_drag_ended(&self) {
|
|
self.0.borrow_mut().scrollbar_drag_start_height.take();
|
|
}
|
|
|
|
/// Set the offset from the scrollbar
|
|
pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
|
|
self.0.borrow_mut().set_offset_from_scrollbar(point);
|
|
}
|
|
|
|
/// Returns the maximum scroll offset according to the items we have measured.
|
|
/// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly.
|
|
pub fn max_offset_for_scrollbar(&self) -> Size<Pixels> {
|
|
let state = self.0.borrow();
|
|
let bounds = state.last_layout_bounds.unwrap_or_default();
|
|
|
|
let height = state
|
|
.scrollbar_drag_start_height
|
|
.unwrap_or_else(|| state.items.summary().height);
|
|
|
|
Size::new(Pixels::ZERO, Pixels::ZERO.max(height - bounds.size.height))
|
|
}
|
|
|
|
/// Returns the current scroll offset adjusted for the scrollbar
|
|
pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
|
|
let state = &self.0.borrow();
|
|
let logical_scroll_top = state.logical_scroll_top();
|
|
|
|
let mut cursor = state.items.cursor::<ListItemSummary>(());
|
|
let summary: ListItemSummary =
|
|
cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right);
|
|
let content_height = state.items.summary().height;
|
|
let drag_offset =
|
|
// if dragging the scrollbar, we want to offset the point if the height changed
|
|
content_height - state.scrollbar_drag_start_height.unwrap_or(content_height);
|
|
let offset = summary.height + logical_scroll_top.offset_in_item - drag_offset;
|
|
|
|
Point::new(px(0.), -offset)
|
|
}
|
|
|
|
/// Return the bounds of the viewport in pixels.
|
|
pub fn viewport_bounds(&self) -> Bounds<Pixels> {
|
|
self.0.borrow().last_layout_bounds.unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
impl StateInner {
|
|
fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
|
|
let mut cursor = self.items.cursor::<ListItemSummary>(());
|
|
cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
|
|
let start_y = cursor.start().height + scroll_top.offset_in_item;
|
|
cursor.seek_forward(&Height(start_y + height), Bias::Left);
|
|
scroll_top.item_ix..cursor.start().count + 1
|
|
}
|
|
|
|
fn scroll(
|
|
&mut self,
|
|
scroll_top: &ListOffset,
|
|
height: Pixels,
|
|
delta: Point<Pixels>,
|
|
current_view: EntityId,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
// Drop scroll events after a reset, since we can't calculate
|
|
// the new logical scroll top without the item heights
|
|
if self.reset {
|
|
return;
|
|
}
|
|
|
|
let padding = self.last_padding.unwrap_or_default();
|
|
let scroll_max =
|
|
(self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
|
|
let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
|
|
.max(px(0.))
|
|
.min(scroll_max);
|
|
|
|
if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
|
|
self.logical_scroll_top = None;
|
|
} else {
|
|
let mut cursor = self.items.cursor::<ListItemSummary>(());
|
|
cursor.seek(&Height(new_scroll_top), Bias::Right);
|
|
let item_ix = cursor.start().count;
|
|
let offset_in_item = new_scroll_top - cursor.start().height;
|
|
self.logical_scroll_top = Some(ListOffset {
|
|
item_ix,
|
|
offset_in_item,
|
|
});
|
|
}
|
|
|
|
if self.scroll_handler.is_some() {
|
|
let visible_range = self.visible_range(height, scroll_top);
|
|
self.scroll_handler.as_mut().unwrap()(
|
|
&ListScrollEvent {
|
|
visible_range,
|
|
count: self.items.summary().count,
|
|
is_scrolled: self.logical_scroll_top.is_some(),
|
|
},
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
|
|
cx.notify(current_view);
|
|
}
|
|
|
|
fn logical_scroll_top(&self) -> ListOffset {
|
|
self.logical_scroll_top
|
|
.unwrap_or_else(|| match self.alignment {
|
|
ListAlignment::Top => ListOffset {
|
|
item_ix: 0,
|
|
offset_in_item: px(0.),
|
|
},
|
|
ListAlignment::Bottom => ListOffset {
|
|
item_ix: self.items.summary().count,
|
|
offset_in_item: px(0.),
|
|
},
|
|
})
|
|
}
|
|
|
|
fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
|
|
let mut cursor = self.items.cursor::<ListItemSummary>(());
|
|
cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right);
|
|
cursor.start().height + logical_scroll_top.offset_in_item
|
|
}
|
|
|
|
fn layout_items(
|
|
&mut self,
|
|
available_width: Option<Pixels>,
|
|
available_height: Pixels,
|
|
padding: &Edges<Pixels>,
|
|
render_item: &mut RenderItemFn,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> LayoutItemsResponse {
|
|
let old_items = self.items.clone();
|
|
let mut measured_items = VecDeque::new();
|
|
let mut item_layouts = VecDeque::new();
|
|
let mut rendered_height = padding.top;
|
|
let mut max_item_width = px(0.);
|
|
let mut scroll_top = self.logical_scroll_top();
|
|
let mut rendered_focused_item = false;
|
|
|
|
let available_item_space = size(
|
|
available_width.map_or(AvailableSpace::MinContent, |width| {
|
|
AvailableSpace::Definite(width)
|
|
}),
|
|
AvailableSpace::MinContent,
|
|
);
|
|
|
|
let mut cursor = old_items.cursor::<Count>(());
|
|
|
|
// Render items after the scroll top, including those in the trailing overdraw
|
|
cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
|
|
for (ix, item) in cursor.by_ref().enumerate() {
|
|
let visible_height = rendered_height - scroll_top.offset_in_item;
|
|
if visible_height >= available_height + self.overdraw {
|
|
break;
|
|
}
|
|
|
|
// Use the previously cached height and focus handle if available
|
|
let mut size = item.size();
|
|
|
|
// If we're within the visible area or the height wasn't cached, render and measure the item's element
|
|
if visible_height < available_height || size.is_none() {
|
|
let item_index = scroll_top.item_ix + ix;
|
|
let mut element = render_item(item_index, window, cx);
|
|
let element_size = element.layout_as_root(available_item_space, window, cx);
|
|
size = Some(element_size);
|
|
if visible_height < available_height {
|
|
item_layouts.push_back(ItemLayout {
|
|
index: item_index,
|
|
element,
|
|
size: element_size,
|
|
});
|
|
if item.contains_focused(window, cx) {
|
|
rendered_focused_item = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
let size = size.unwrap();
|
|
rendered_height += size.height;
|
|
max_item_width = max_item_width.max(size.width);
|
|
measured_items.push_back(ListItem::Measured {
|
|
size,
|
|
focus_handle: item.focus_handle(),
|
|
});
|
|
}
|
|
rendered_height += padding.bottom;
|
|
|
|
// Prepare to start walking upward from the item at the scroll top.
|
|
cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
|
|
|
|
// If the rendered items do not fill the visible region, then adjust
|
|
// the scroll top upward.
|
|
if rendered_height - scroll_top.offset_in_item < available_height {
|
|
while rendered_height < available_height {
|
|
cursor.prev();
|
|
if let Some(item) = cursor.item() {
|
|
let item_index = cursor.start().0;
|
|
let mut element = render_item(item_index, window, cx);
|
|
let element_size = element.layout_as_root(available_item_space, window, cx);
|
|
let focus_handle = item.focus_handle();
|
|
rendered_height += element_size.height;
|
|
measured_items.push_front(ListItem::Measured {
|
|
size: element_size,
|
|
focus_handle,
|
|
});
|
|
item_layouts.push_front(ItemLayout {
|
|
index: item_index,
|
|
element,
|
|
size: element_size,
|
|
});
|
|
if item.contains_focused(window, cx) {
|
|
rendered_focused_item = true;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
scroll_top = ListOffset {
|
|
item_ix: cursor.start().0,
|
|
offset_in_item: rendered_height - available_height,
|
|
};
|
|
|
|
match self.alignment {
|
|
ListAlignment::Top => {
|
|
scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
|
|
self.logical_scroll_top = Some(scroll_top);
|
|
}
|
|
ListAlignment::Bottom => {
|
|
scroll_top = ListOffset {
|
|
item_ix: cursor.start().0,
|
|
offset_in_item: rendered_height - available_height,
|
|
};
|
|
self.logical_scroll_top = None;
|
|
}
|
|
};
|
|
}
|
|
|
|
// Measure items in the leading overdraw
|
|
let mut leading_overdraw = scroll_top.offset_in_item;
|
|
while leading_overdraw < self.overdraw {
|
|
cursor.prev();
|
|
if let Some(item) = cursor.item() {
|
|
let size = if let ListItem::Measured { size, .. } = item {
|
|
*size
|
|
} else {
|
|
let mut element = render_item(cursor.start().0, window, cx);
|
|
element.layout_as_root(available_item_space, window, cx)
|
|
};
|
|
|
|
leading_overdraw += size.height;
|
|
measured_items.push_front(ListItem::Measured {
|
|
size,
|
|
focus_handle: item.focus_handle(),
|
|
});
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
|
|
let mut cursor = old_items.cursor::<Count>(());
|
|
let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right);
|
|
new_items.extend(measured_items, ());
|
|
cursor.seek(&Count(measured_range.end), Bias::Right);
|
|
new_items.append(cursor.suffix(), ());
|
|
self.items = new_items;
|
|
|
|
// If none of the visible items are focused, check if an off-screen item is focused
|
|
// and include it to be rendered after the visible items so keyboard interaction continues
|
|
// to work for it.
|
|
if !rendered_focused_item {
|
|
let mut cursor = self
|
|
.items
|
|
.filter::<_, Count>((), |summary| summary.has_focus_handles);
|
|
cursor.next();
|
|
while let Some(item) = cursor.item() {
|
|
if item.contains_focused(window, cx) {
|
|
let item_index = cursor.start().0;
|
|
let mut element = render_item(cursor.start().0, window, cx);
|
|
let size = element.layout_as_root(available_item_space, window, cx);
|
|
item_layouts.push_back(ItemLayout {
|
|
index: item_index,
|
|
element,
|
|
size,
|
|
});
|
|
break;
|
|
}
|
|
cursor.next();
|
|
}
|
|
}
|
|
|
|
LayoutItemsResponse {
|
|
max_item_width,
|
|
scroll_top,
|
|
item_layouts,
|
|
}
|
|
}
|
|
|
|
fn prepaint_items(
|
|
&mut self,
|
|
bounds: Bounds<Pixels>,
|
|
padding: Edges<Pixels>,
|
|
autoscroll: bool,
|
|
render_item: &mut RenderItemFn,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Result<LayoutItemsResponse, ListOffset> {
|
|
window.transact(|window| {
|
|
let mut layout_response = self.layout_items(
|
|
Some(bounds.size.width),
|
|
bounds.size.height,
|
|
&padding,
|
|
render_item,
|
|
window,
|
|
cx,
|
|
);
|
|
|
|
// Avoid honoring autoscroll requests from elements other than our children.
|
|
window.take_autoscroll();
|
|
|
|
// Only paint the visible items, if there is actually any space for them (taking padding into account)
|
|
if bounds.size.height > padding.top + padding.bottom {
|
|
let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
|
|
item_origin.y -= layout_response.scroll_top.offset_in_item;
|
|
for item in &mut layout_response.item_layouts {
|
|
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
|
item.element.prepaint_at(item_origin, window, cx);
|
|
});
|
|
|
|
if let Some(autoscroll_bounds) = window.take_autoscroll()
|
|
&& autoscroll
|
|
{
|
|
if autoscroll_bounds.top() < bounds.top() {
|
|
return Err(ListOffset {
|
|
item_ix: item.index,
|
|
offset_in_item: autoscroll_bounds.top() - item_origin.y,
|
|
});
|
|
} else if autoscroll_bounds.bottom() > bounds.bottom() {
|
|
let mut cursor = self.items.cursor::<Count>(());
|
|
cursor.seek(&Count(item.index), Bias::Right);
|
|
let mut height = bounds.size.height - padding.top - padding.bottom;
|
|
|
|
// Account for the height of the element down until the autoscroll bottom.
|
|
height -= autoscroll_bounds.bottom() - item_origin.y;
|
|
|
|
// Keep decreasing the scroll top until we fill all the available space.
|
|
while height > Pixels::ZERO {
|
|
cursor.prev();
|
|
let Some(item) = cursor.item() else { break };
|
|
|
|
let size = item.size().unwrap_or_else(|| {
|
|
let mut item = render_item(cursor.start().0, window, cx);
|
|
let item_available_size =
|
|
size(bounds.size.width.into(), AvailableSpace::MinContent);
|
|
item.layout_as_root(item_available_size, window, cx)
|
|
});
|
|
height -= size.height;
|
|
}
|
|
|
|
return Err(ListOffset {
|
|
item_ix: cursor.start().0,
|
|
offset_in_item: if height < Pixels::ZERO {
|
|
-height
|
|
} else {
|
|
Pixels::ZERO
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
item_origin.y += item.size.height;
|
|
}
|
|
} else {
|
|
layout_response.item_layouts.clear();
|
|
}
|
|
|
|
Ok(layout_response)
|
|
})
|
|
}
|
|
|
|
// Scrollbar support
|
|
|
|
fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
|
|
let Some(bounds) = self.last_layout_bounds else {
|
|
return;
|
|
};
|
|
let height = bounds.size.height;
|
|
|
|
let padding = self.last_padding.unwrap_or_default();
|
|
let content_height = self.items.summary().height;
|
|
let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
|
|
let drag_offset =
|
|
// if dragging the scrollbar, we want to offset the point if the height changed
|
|
content_height - self.scrollbar_drag_start_height.unwrap_or(content_height);
|
|
let new_scroll_top = (point.y - drag_offset).abs().max(px(0.)).min(scroll_max);
|
|
|
|
if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
|
|
self.logical_scroll_top = None;
|
|
} else {
|
|
let mut cursor = self.items.cursor::<ListItemSummary>(());
|
|
cursor.seek(&Height(new_scroll_top), Bias::Right);
|
|
|
|
let item_ix = cursor.start().count;
|
|
let offset_in_item = new_scroll_top - cursor.start().height;
|
|
self.logical_scroll_top = Some(ListOffset {
|
|
item_ix,
|
|
offset_in_item,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for ListItem {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Unmeasured { .. } => write!(f, "Unrendered"),
|
|
Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An offset into the list's items, in terms of the item index and the number
|
|
/// of pixels off the top left of the item.
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
pub struct ListOffset {
|
|
/// The index of an item in the list
|
|
pub item_ix: usize,
|
|
/// The number of pixels to offset from the item index.
|
|
pub offset_in_item: Pixels,
|
|
}
|
|
|
|
impl Element for List {
|
|
type RequestLayoutState = ();
|
|
type PrepaintState = ListPrepaintState;
|
|
|
|
fn id(&self) -> Option<crate::ElementId> {
|
|
None
|
|
}
|
|
|
|
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
|
None
|
|
}
|
|
|
|
fn request_layout(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
_inspector_id: Option<&InspectorElementId>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> (crate::LayoutId, Self::RequestLayoutState) {
|
|
let layout_id = match self.sizing_behavior {
|
|
ListSizingBehavior::Infer => {
|
|
let mut style = Style::default();
|
|
style.overflow.y = Overflow::Scroll;
|
|
style.refine(&self.style);
|
|
window.with_text_style(style.text_style().cloned(), |window| {
|
|
let state = &mut *self.state.0.borrow_mut();
|
|
|
|
let available_height = if let Some(last_bounds) = state.last_layout_bounds {
|
|
last_bounds.size.height
|
|
} else {
|
|
// If we don't have the last layout bounds (first render),
|
|
// we might just use the overdraw value as the available height to layout enough items.
|
|
state.overdraw
|
|
};
|
|
let padding = style.padding.to_pixels(
|
|
state.last_layout_bounds.unwrap_or_default().size.into(),
|
|
window.rem_size(),
|
|
);
|
|
|
|
let layout_response = state.layout_items(
|
|
None,
|
|
available_height,
|
|
&padding,
|
|
&mut self.render_item,
|
|
window,
|
|
cx,
|
|
);
|
|
let max_element_width = layout_response.max_item_width;
|
|
|
|
let summary = state.items.summary();
|
|
let total_height = summary.height;
|
|
|
|
window.request_measured_layout(
|
|
style,
|
|
move |known_dimensions, available_space, _window, _cx| {
|
|
let width =
|
|
known_dimensions
|
|
.width
|
|
.unwrap_or(match available_space.width {
|
|
AvailableSpace::Definite(x) => x,
|
|
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
|
max_element_width
|
|
}
|
|
});
|
|
let height = match available_space.height {
|
|
AvailableSpace::Definite(height) => total_height.min(height),
|
|
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
|
total_height
|
|
}
|
|
};
|
|
size(width, height)
|
|
},
|
|
)
|
|
})
|
|
}
|
|
ListSizingBehavior::Auto => {
|
|
let mut style = Style::default();
|
|
style.refine(&self.style);
|
|
window.with_text_style(style.text_style().cloned(), |window| {
|
|
window.request_layout(style, None, cx)
|
|
})
|
|
}
|
|
};
|
|
(layout_id, ())
|
|
}
|
|
|
|
fn prepaint(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
_inspector_id: Option<&InspectorElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
_: &mut Self::RequestLayoutState,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> ListPrepaintState {
|
|
let state = &mut *self.state.0.borrow_mut();
|
|
state.reset = false;
|
|
|
|
let mut style = Style::default();
|
|
style.refine(&self.style);
|
|
|
|
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
|
|
|
// If the width of the list has changed, invalidate all cached item heights
|
|
if state
|
|
.last_layout_bounds
|
|
.is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width)
|
|
{
|
|
let new_items = SumTree::from_iter(
|
|
state.items.iter().map(|item| ListItem::Unmeasured {
|
|
focus_handle: item.focus_handle(),
|
|
}),
|
|
(),
|
|
);
|
|
|
|
state.items = new_items;
|
|
}
|
|
|
|
let padding = style
|
|
.padding
|
|
.to_pixels(bounds.size.into(), window.rem_size());
|
|
let layout =
|
|
match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) {
|
|
Ok(layout) => layout,
|
|
Err(autoscroll_request) => {
|
|
state.logical_scroll_top = Some(autoscroll_request);
|
|
state
|
|
.prepaint_items(bounds, padding, false, &mut self.render_item, window, cx)
|
|
.unwrap()
|
|
}
|
|
};
|
|
|
|
state.last_layout_bounds = Some(bounds);
|
|
state.last_padding = Some(padding);
|
|
ListPrepaintState { hitbox, layout }
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
_inspector_id: Option<&InspectorElementId>,
|
|
bounds: Bounds<crate::Pixels>,
|
|
_: &mut Self::RequestLayoutState,
|
|
prepaint: &mut Self::PrepaintState,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
let current_view = window.current_view();
|
|
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
|
for item in &mut prepaint.layout.item_layouts {
|
|
item.element.paint(window, cx);
|
|
}
|
|
});
|
|
|
|
let list_state = self.state.clone();
|
|
let height = bounds.size.height;
|
|
let scroll_top = prepaint.layout.scroll_top;
|
|
let hitbox_id = prepaint.hitbox.id;
|
|
let mut accumulated_scroll_delta = ScrollDelta::default();
|
|
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
|
|
if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) {
|
|
accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
|
|
let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
|
|
list_state.0.borrow_mut().scroll(
|
|
&scroll_top,
|
|
height,
|
|
pixel_delta,
|
|
current_view,
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
impl IntoElement for List {
|
|
type Element = Self;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Styled for List {
|
|
fn style(&mut self) -> &mut StyleRefinement {
|
|
&mut self.style
|
|
}
|
|
}
|
|
|
|
impl sum_tree::Item for ListItem {
|
|
type Summary = ListItemSummary;
|
|
|
|
fn summary(&self, _: ()) -> Self::Summary {
|
|
match self {
|
|
ListItem::Unmeasured { focus_handle } => ListItemSummary {
|
|
count: 1,
|
|
rendered_count: 0,
|
|
unrendered_count: 1,
|
|
height: px(0.),
|
|
has_focus_handles: focus_handle.is_some(),
|
|
},
|
|
ListItem::Measured {
|
|
size, focus_handle, ..
|
|
} => ListItemSummary {
|
|
count: 1,
|
|
rendered_count: 1,
|
|
unrendered_count: 0,
|
|
height: size.height,
|
|
has_focus_handles: focus_handle.is_some(),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl sum_tree::ContextLessSummary for ListItemSummary {
|
|
fn zero() -> Self {
|
|
Default::default()
|
|
}
|
|
|
|
fn add_summary(&mut self, summary: &Self) {
|
|
self.count += summary.count;
|
|
self.rendered_count += summary.rendered_count;
|
|
self.unrendered_count += summary.unrendered_count;
|
|
self.height += summary.height;
|
|
self.has_focus_handles |= summary.has_focus_handles;
|
|
}
|
|
}
|
|
|
|
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
|
|
fn zero(_cx: ()) -> Self {
|
|
Default::default()
|
|
}
|
|
|
|
fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
|
|
self.0 += summary.count;
|
|
}
|
|
}
|
|
|
|
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
|
|
fn zero(_cx: ()) -> Self {
|
|
Default::default()
|
|
}
|
|
|
|
fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
|
|
self.0 += summary.height;
|
|
}
|
|
}
|
|
|
|
impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Count {
|
|
fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
|
|
self.0.partial_cmp(&other.count).unwrap()
|
|
}
|
|
}
|
|
|
|
impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height {
|
|
fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
|
|
self.0.partial_cmp(&other.height).unwrap()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
|
|
use gpui::{ScrollDelta, ScrollWheelEvent};
|
|
|
|
use crate::{self as gpui, TestAppContext};
|
|
|
|
#[gpui::test]
|
|
fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
|
|
use crate::{
|
|
AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
|
|
list, point, px, size,
|
|
};
|
|
|
|
let cx = cx.add_empty_window();
|
|
|
|
let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
|
|
|
|
// Ensure that the list is scrolled to the top
|
|
state.scroll_to(gpui::ListOffset {
|
|
item_ix: 0,
|
|
offset_in_item: px(0.0),
|
|
});
|
|
|
|
struct TestView(ListState);
|
|
impl Render for TestView {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
list(self.0.clone(), |_, _, _| {
|
|
div().h(px(10.)).w_full().into_any()
|
|
})
|
|
.w_full()
|
|
.h_full()
|
|
}
|
|
}
|
|
|
|
// Paint
|
|
cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
|
|
cx.new(|_| TestView(state.clone()))
|
|
});
|
|
|
|
// Reset
|
|
state.reset(5);
|
|
|
|
// And then receive a scroll event _before_ the next paint
|
|
cx.simulate_event(ScrollWheelEvent {
|
|
position: point(px(1.), px(1.)),
|
|
delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
|
|
..Default::default()
|
|
});
|
|
|
|
// Scroll position should stay at the top of the list
|
|
assert_eq!(state.logical_scroll_top().item_ix, 0);
|
|
assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) {
|
|
use crate::{
|
|
AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
|
|
list, point, px, size,
|
|
};
|
|
|
|
let cx = cx.add_empty_window();
|
|
|
|
let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
|
|
|
|
struct TestView(ListState);
|
|
impl Render for TestView {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
list(self.0.clone(), |_, _, _| {
|
|
div().h(px(20.)).w_full().into_any()
|
|
})
|
|
.w_full()
|
|
.h_full()
|
|
}
|
|
}
|
|
|
|
// Paint
|
|
cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
|
|
cx.new(|_| TestView(state.clone()))
|
|
});
|
|
|
|
// Test positive distance: start at item 1, move down 30px
|
|
state.scroll_by(px(30.));
|
|
|
|
// Should move to item 2
|
|
let offset = state.logical_scroll_top();
|
|
assert_eq!(offset.item_ix, 1);
|
|
assert_eq!(offset.offset_in_item, px(10.));
|
|
|
|
// Test negative distance: start at item 2, move up 30px
|
|
state.scroll_by(px(-30.));
|
|
|
|
// Should move back to item 1
|
|
let offset = state.logical_scroll_top();
|
|
assert_eq!(offset.item_ix, 0);
|
|
assert_eq!(offset.offset_in_item, px(0.));
|
|
|
|
// Test zero distance
|
|
state.scroll_by(px(0.));
|
|
let offset = state.logical_scroll_top();
|
|
assert_eq!(offset.item_ix, 0);
|
|
assert_eq!(offset.offset_in_item, px(0.));
|
|
}
|
|
}
|