use std::{
cell::RefCell,
cmp,
ops::{Deref, Range},
rc::Rc,
};
use gpui::{
Along, AnyElement, App, AvailableSpace, Axis, Bounds, ContentMask, 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, div, point, px, size,
};
use smallvec::SmallVec;
use crate::{PixelsExt, ScrollbarHandle};
type RenderItemsCallback =
dyn for<'a> Fn(Range<usize>, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>;
struct VirtualListScrollHandleState {
axis: Axis,
items_count: usize,
pub deferred_scroll_to_item: Option<DeferredScrollToItem>,
}
#[derive(Clone)]
pub struct VirtualListScrollHandle {
state: Rc<RefCell<VirtualListScrollHandleState>>,
base_handle: ScrollHandle,
}
impl From<ScrollHandle> for VirtualListScrollHandle {
fn from(handle: ScrollHandle) -> Self {
let mut this = VirtualListScrollHandle::new();
this.base_handle = handle;
this
}
}
impl AsRef<ScrollHandle> for VirtualListScrollHandle {
fn as_ref(&self) -> &ScrollHandle {
&self.base_handle
}
}
impl ScrollbarHandle for VirtualListScrollHandle {
fn offset(&self) -> Point<Pixels> {
self.base_handle.offset()
}
fn set_offset(&self, offset: Point<Pixels>) {
self.base_handle.set_offset(offset);
}
fn content_size(&self) -> Size<Pixels> {
self.base_handle.content_size()
}
}
impl Deref for VirtualListScrollHandle {
type Target = ScrollHandle;
fn deref(&self) -> &Self::Target {
&self.base_handle
}
}
impl VirtualListScrollHandle {
pub fn new() -> Self {
VirtualListScrollHandle {
state: Rc::new(RefCell::new(VirtualListScrollHandleState {
axis: Axis::Vertical,
items_count: 0,
deferred_scroll_to_item: None,
})),
base_handle: ScrollHandle::default(),
}
}
pub fn base_handle(&self) -> &ScrollHandle {
&self.base_handle
}
pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
self.scroll_to_item_with_offset(ix, strategy, 0);
}
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,
scroll_strict: false,
});
}
pub fn scroll_to_bottom(&self) {
let items_count = self.state.borrow().items_count;
self.scroll_to_item(items_count.saturating_sub(1), ScrollStrategy::Top);
}
}
impl Default for VirtualListScrollHandle {
fn default() -> Self {
Self::new()
}
}
#[inline]
pub fn v_virtual_list<R, V>(
view: Entity<V>, id: impl Into<ElementId>, item_sizes: Rc<Vec<Size<Pixels>>>,
f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
) -> VirtualList
where
R: IntoElement,
V: Render, {
virtual_list(view, id, Axis::Vertical, item_sizes, f)
}
#[inline]
pub fn h_virtual_list<R, V>(
view: Entity<V>, id: impl Into<ElementId>, item_sizes: Rc<Vec<Size<Pixels>>>,
f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
) -> VirtualList
where
R: IntoElement,
V: Render, {
virtual_list(view, id, Axis::Horizontal, item_sizes, f)
}
pub(crate) fn virtual_list<R, V>(
view: Entity<V>, id: impl Into<ElementId>, axis: Axis, item_sizes: Rc<Vec<Size<Pixels>>>,
f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
) -> VirtualList
where
R: IntoElement,
V: Render, {
let id: ElementId = id.into();
let scroll_handle = VirtualListScrollHandle::new();
let render_range = move |visible_range, window: &mut Window, cx: &mut App| {
view.update(cx, |this, cx| {
f(this, visible_range, window, cx)
.into_iter()
.map(|component| component.into_any_element())
.collect()
})
};
VirtualList {
id: id.clone(),
axis,
base: div()
.id(id)
.size_full()
.overflow_scroll()
.track_scroll(&scroll_handle),
scroll_handle,
items_count: item_sizes.len(),
item_sizes,
render_items: Box::new(render_range),
sizing_behavior: ListSizingBehavior::default(),
}
}
pub struct VirtualList {
id: ElementId,
axis: Axis,
base: Stateful<Div>,
scroll_handle: VirtualListScrollHandle,
items_count: usize,
item_sizes: Rc<Vec<Size<Pixels>>>,
render_items: Box<RenderItemsCallback>,
sizing_behavior: ListSizingBehavior,
}
fn first_visible_index(origins: &[Pixels], sizes: &[Pixels], visible_start: Pixels) -> usize {
let mut left = 0;
let mut right = sizes.len();
while left < right {
let mid = (left + right) / 2;
let item_end = origins[mid] + sizes[mid];
if item_end <= visible_start {
left = mid + 1;
} else {
right = mid;
}
}
left
}
fn last_visible_index(origins: &[Pixels], visible_end: Pixels, items_count: usize) -> usize {
cmp::min(
origins
.partition_point(|origin| *origin < visible_end)
.saturating_add(1),
items_count,
)
}
impl Styled for VirtualList {
fn style(&mut self) -> &mut StyleRefinement {
self.base.style()
}
}
impl VirtualList {
pub fn track_scroll(mut self, scroll_handle: &VirtualListScrollHandle) -> Self {
self.base = self.base.track_scroll(scroll_handle);
self.scroll_handle = scroll_handle.clone();
self
}
pub(crate) fn with_scroll_handle(mut self, scroll_handle: &VirtualListScrollHandle) -> Self {
self.base = div().id(self.id.clone()).size_full();
self.scroll_handle = scroll_handle.clone();
self
}
pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
self.sizing_behavior = behavior;
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 matches!(self.axis, Axis::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()
}
}
_ => {
if matches!(self.axis, Axis::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
}
fn measure_item(
&self, list_width: Option<Pixels>, window: &mut Window, cx: &mut App,
) -> Size<Pixels> {
if self.items_count == 0 {
return Size::default();
}
let item_ix = 0;
let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx);
let Some(mut item_to_measure) = items.pop() else {
return Size::default();
};
let available_space = size(
list_width.map_or(AvailableSpace::MinContent, |width| {
AvailableSpace::Definite(width)
}),
AvailableSpace::MinContent,
);
item_to_measure.layout_as_root(available_space, window, cx)
}
}
pub struct VirtualListFrameState {
items: SmallVec<[AnyElement; 32]>,
size_layout: ItemSizeLayout,
}
#[derive(Default, Clone)]
pub struct ItemSizeLayout {
items_sizes: Rc<Vec<Size<Pixels>>>,
content_size: Size<Pixels>,
sizes: Vec<Pixels>,
origins: Vec<Pixels>,
last_layout_bounds: Bounds<Pixels>,
}
impl IntoElement for VirtualList {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for VirtualList {
type RequestLayoutState = VirtualListFrameState;
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self, global_id: Option<&GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>, window: &mut Window, cx: &mut App,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
let rem_size = window.rem_size();
let font_size = window.text_style().font_size.to_pixels(rem_size);
let mut size_layout = ItemSizeLayout::default();
let longest_item_size = self.measure_item(None, window, cx);
let layout_id = self.base.interactivity().request_layout(
global_id,
inspector_id,
window,
cx,
|style, window, cx| {
size_layout = window.with_element_state(
global_id.unwrap(),
|state: Option<ItemSizeLayout>, _window| {
let mut state = state.unwrap_or_default();
let gap = style
.gap
.along(self.axis)
.to_pixels(font_size.into(), rem_size);
if state.items_sizes != self.item_sizes {
state.items_sizes = self.item_sizes.clone();
state.sizes = self
.item_sizes
.iter()
.enumerate()
.map(|(i, size)| {
let size = size.along(self.axis);
if i + 1 == self.items_count {
size
} else {
size + gap
}
})
.collect::<Vec<_>>();
state.origins = state
.sizes
.iter()
.scan(px(0.), |cumulative, size| match self.axis {
Axis::Horizontal => {
let x = *cumulative;
*cumulative += *size;
Some(x)
}
Axis::Vertical => {
let y = *cumulative;
*cumulative += *size;
Some(y)
}
})
.collect::<Vec<_>>();
state.content_size = if matches!(self.axis, Axis::Horizontal) {
Size {
width: px(state.sizes.iter().map(|size| size.as_f32()).sum::<f32>()),
height: longest_item_size.height,
}
} else {
Size {
width: longest_item_size.width,
height: px(state.sizes.iter().map(|size| size.as_f32()).sum::<f32>()),
}
};
}
(state.clone(), state)
},
);
let axis = self.axis;
match self.sizing_behavior {
ListSizingBehavior::Infer => {
window.with_text_style(style.text_style().cloned(), |window| {
let size_layout = size_layout.clone();
window.request_measured_layout(style, {
move |known_dimensions, available_space, _, _| {
let mut size = Size::default();
if matches!(axis, Axis::Horizontal) {
size.width = known_dimensions
.width
.unwrap_or(match available_space.width {
AvailableSpace::Definite(x) => x,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
size_layout.content_size.width
}
});
size.height = known_dimensions
.width
.unwrap_or(match available_space.height {
AvailableSpace::Definite(x) => x,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
size_layout.content_size.height
}
});
} else {
size.width = known_dimensions
.width
.unwrap_or(match available_space.width {
AvailableSpace::Definite(x) => x,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
size_layout.content_size.width
}
});
size.height = known_dimensions
.height
.unwrap_or(match available_space.height {
AvailableSpace::Definite(x) => x,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
size_layout.content_size.height
}
});
}
size
}
})
})
}
ListSizingBehavior::Auto => window
.with_text_style(style.text_style().cloned(), |window| {
window.request_layout(style, None, cx)
}),
}
},
);
(
layout_id,
VirtualListFrameState {
items: SmallVec::new(),
size_layout,
},
)
}
fn prepaint(
&mut self, global_id: Option<&GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>, bounds: Bounds<Pixels>,
layout: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App,
) -> Self::PrepaintState {
layout.size_layout.last_layout_bounds = bounds;
let style = self
.base
.interactivity()
.compute_style(global_id, None, window, cx);
let border_widths = style.border_widths.to_pixels(window.rem_size());
let paddings = style
.padding
.to_pixels(bounds.size.into(), window.rem_size());
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,
),
);
let items_bounds = item_origins
.iter()
.enumerate()
.map(|(i, &origin)| {
let item_size = item_sizes[i];
Bounds {
origin: match self.axis {
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, content_bounds.size.height),
Axis::Vertical => size(content_bounds.size.width, item_size),
},
}
})
.collect::<Vec<_>>();
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,
);
}
scroll_offset = scroll_offset
.max(&point(
content_bounds.size.width - layout.size_layout.content_size.width,
content_bounds.size.height - layout.size_layout.content_size.height,
))
.min(&point(px(0.), px(0.)));
if scroll_offset != self.scroll_handle.offset() {
self.scroll_handle.set_offset(scroll_offset);
}
self.base.interactivity().prepaint(
global_id,
inspector_id,
bounds,
layout.size_layout.content_size,
window,
cx,
|_style, _, hitbox, window, cx| {
if self.items_count > 0 {
let min_scroll_offset =
content_bounds.size.along(self.axis) - layout.size_layout.content_size.along(self.axis);
let is_scrolled = !scroll_offset.along(self.axis).is_zero();
if is_scrolled {
match self.axis {
Axis::Horizontal if scroll_offset.x < min_scroll_offset => {
scroll_offset.x = min_scroll_offset;
self.scroll_handle.set_offset(scroll_offset);
}
Axis::Vertical if scroll_offset.y < min_scroll_offset => {
scroll_offset.y = min_scroll_offset;
self.scroll_handle.set_offset(scroll_offset);
}
_ => {}
}
}
let (visible_start, visible_end) = match self.axis {
Axis::Horizontal => (
-(scroll_offset.x + paddings.left),
-scroll_offset.x + content_bounds.size.width,
),
Axis::Vertical => (
-(scroll_offset.y + paddings.top),
-scroll_offset.y + content_bounds.size.height,
),
};
let first_visible_element_ix =
first_visible_index(item_origins, item_sizes, visible_start);
let last_visible_element_ix =
last_visible_index(item_origins, visible_end, self.items_count);
let visible_range =
first_visible_element_ix..cmp::min(last_visible_element_ix, self.items_count);
let items = (self.render_items)(visible_range.clone(), window, cx);
let content_mask = ContentMask { bounds };
window.with_content_mask(Some(content_mask), |window| {
for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
let item_origin = match self.axis {
Axis::Horizontal => {
content_bounds.origin + point(item_origins[ix] + scroll_offset.x, scroll_offset.y)
}
Axis::Vertical => {
content_bounds.origin + point(scroll_offset.x, item_origins[ix] + scroll_offset.y)
}
};
let available_space = match self.axis {
Axis::Horizontal => size(
AvailableSpace::Definite(item_sizes[ix]),
AvailableSpace::Definite(content_bounds.size.height),
),
Axis::Vertical => size(
AvailableSpace::Definite(content_bounds.size.width),
AvailableSpace::Definite(item_sizes[ix]),
),
};
item.layout_as_root(available_space, window, cx);
item.prepaint_at(item_origin, window, cx);
layout.items.push(item);
}
});
}
hitbox
},
)
}
fn paint(
&mut self, global_id: Option<&GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>, bounds: Bounds<Pixels>,
layout: &mut Self::RequestLayoutState, hitbox: &mut Self::PrepaintState, window: &mut Window,
cx: &mut App,
) {
self.base.interactivity().paint(
global_id,
inspector_id,
bounds,
hitbox.as_ref(),
window,
cx,
|_, window, cx| {
for item in &mut layout.items {
item.paint(window, cx);
}
},
)
}
}