//! Vistual List for render a large number of differently sized rows/columns. //! //! > NOTE: This must ensure each column width or row height. //! //! Only visible range are rendered for performance reasons. //! //! Inspired by `gpui::uniform_list`. //! https://github.com/zed-industries/zed/blob/0ae1603610ab6b265bdfbee7b8dbc23c5ab06edc/crates/gpui/src/elements/uniform_list.rs //! //! Unlike the `uniform_list`, the each item can have different size. //! //! This is useful for more complex layout, for example, a table with different row height. use std::{ cell::RefCell, cmp, ops::{Deref, Range}, rc::Rc, }; use gpui::{ div, point, px, size, 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, }; use smallvec::SmallVec; use crate::{scroll::ScrollHandleOffsetable, AxisExt, PixelsExt}; struct VirtualListScrollHandleState { axis: Axis, items_count: usize, pub deferred_scroll_to_item: Option, } #[derive(Clone)] pub struct VirtualListScrollHandle { state: Rc>, base_handle: ScrollHandle, } impl From for VirtualListScrollHandle { fn from(handle: ScrollHandle) -> Self { let mut this = VirtualListScrollHandle::new(); this.base_handle = handle; this } } impl AsRef for VirtualListScrollHandle { fn as_ref(&self) -> &ScrollHandle { &self.base_handle } } impl ScrollHandleOffsetable for VirtualListScrollHandle { fn offset(&self) -> Point { self.base_handle.offset() } fn set_offset(&self, offset: Point) { self.base_handle.set_offset(offset); } fn content_size(&self) -> Size { 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 } /// Scroll to the item at the given index. pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) { self.scroll_to_item_with_offset(ix, strategy, 0); } /// 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, scroll_strict: false, }); } /// Scrolls to the bottom of the list. 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); } } /// Create a [`VirtualList`] in vertical direction. /// /// This is like `uniform_list` in GPUI, but support two axis. /// /// The `item_sizes` is the size of each column. /// /// See also [`h_virtual_list`] #[inline] pub fn v_virtual_list( view: Entity, id: impl Into, item_sizes: Rc>>, f: impl 'static + Fn(&mut V, Range, &mut Window, &mut Context) -> Vec, ) -> VirtualList where R: IntoElement, V: Render, { virtual_list(view, id, Axis::Vertical, item_sizes, f) } /// Create a [`VirtualList`] in horizontal direction. /// /// See also [`v_virtual_list`] #[inline] pub fn h_virtual_list( view: Entity, id: impl Into, item_sizes: Rc>>, f: impl 'static + Fn(&mut V, Range, &mut Window, &mut Context) -> Vec, ) -> VirtualList where R: IntoElement, V: Render, { virtual_list(view, id, Axis::Horizontal, item_sizes, f) } pub(crate) fn virtual_list( view: Entity, id: impl Into, axis: Axis, item_sizes: Rc>>, f: impl 'static + Fn(&mut V, Range, &mut Window, &mut Context) -> Vec, ) -> 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(), } } /// VirtualList component for rendering a large number of differently sized items. pub struct VirtualList { id: ElementId, axis: Axis, base: Stateful
, scroll_handle: VirtualListScrollHandle, items_count: usize, item_sizes: Rc>>, render_items: Box< dyn for<'a> Fn(Range, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>, >, sizing_behavior: ListSizingBehavior, } 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 } /// Set the sizing behavior for the list. pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self { self.sizing_behavior = behavior; self } /// Specify for table. /// /// Table is special, because the `scroll_handle` is based on Table head (That is not a virtual list). 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 } fn scroll_to_deferred_item( &self, scroll_offset: Point, items_bounds: &[Bounds], content_bounds: &Bounds, scroll_to_item: DeferredScrollToItem, ) -> Point { 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]. pub struct VirtualListFrameState { /// Visible items to be painted. items: SmallVec<[AnyElement; 32]>, size_layout: ItemSizeLayout, } #[derive(Default, Clone)] pub struct ItemSizeLayout { items_sizes: Rc>>, content_size: Size, sizes: Vec, origins: Vec, last_layout_bounds: Bounds, } impl IntoElement for VirtualList { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for VirtualList { type RequestLayoutState = VirtualListFrameState; type PrepaintState = Option; fn id(&self) -> Option { 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 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, _window| { let mut state = state.unwrap_or(ItemSizeLayout::default()); // Including the gap between items for calculate the item size 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(); // Prepare each item's size by axis 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::>(); // Prepare each item's origin by axis 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::>(); state.content_size = if self.axis.is_horizontal() { Size { width: px(state .sizes .iter() .map(|size| size.as_f32()) .sum::()), height: state .items_sizes .get(0) .map_or(px(0.), |size| size.height), } } else { Size { width: state .items_sizes .get(0) .map_or(px(0.), |size| size.width), height: px(state .sizes .iter() .map(|size| size.as_f32()) .sum::()), } }; } (state.clone(), state) }, ); let axis = self.axis; let layout_id = 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 axis.is_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 }, ); ( layout_id, VirtualListFrameState { items: SmallVec::new(), size_layout, }, ) } fn prepaint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&gpui::InspectorElementId>, bounds: Bounds, 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, ), ); // Update scroll_handle with the item bounds 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::>(); 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.min(&point(px(0.), px(0.))); 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); // Do not trigger scrolling if the content is smaller than the container. if min_scroll_offset.as_f32() >= 0. { scroll_offset.x = px(0.); scroll_offset.y = px(0.); } 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; } Axis::Vertical if scroll_offset.y < min_scroll_offset => { scroll_offset.y = min_scroll_offset; } _ => {} } } let (first_visible_element_ix, last_visible_element_ix) = match self.axis { Axis::Horizontal => { let mut cumulative_size = px(0.); let mut first_visible_element_ix = 0; for (i, &size) in item_sizes.iter().enumerate() { cumulative_size += size; if cumulative_size > -(scroll_offset.x + paddings.left) { first_visible_element_ix = i; break; } } cumulative_size = px(0.); let mut last_visible_element_ix = 0; for (i, &size) in item_sizes.iter().enumerate() { cumulative_size += size; if cumulative_size > (-scroll_offset.x + content_bounds.size.width) { last_visible_element_ix = i + 1; break; } } if last_visible_element_ix == 0 { last_visible_element_ix = self.items_count; } else { last_visible_element_ix += 1; } (first_visible_element_ix, last_visible_element_ix) } Axis::Vertical => { let mut cumulative_size = px(0.); let mut first_visible_element_ix = 0; for (i, &size) in item_sizes.iter().enumerate() { cumulative_size += size; if cumulative_size > -(scroll_offset.y + paddings.top) { first_visible_element_ix = i; break; } } cumulative_size = px(0.); let mut last_visible_element_ix = 0; for (i, &size) in item_sizes.iter().enumerate() { cumulative_size += size; if cumulative_size > (-scroll_offset.y + content_bounds.size.height) { last_visible_element_ix = i + 1; break; } } if last_visible_element_ix == 0 { last_visible_element_ix = self.items_count; } else { last_visible_element_ix += 1; } (first_visible_element_ix, last_visible_element_ix) } }; 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, 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); } }, ) } }