diff --git a/crates/story/src/scrollable_story.rs b/crates/story/src/scrollable_story.rs index 0e0af84a..2b667638 100644 --- a/crates/story/src/scrollable_story.rs +++ b/crates/story/src/scrollable_story.rs @@ -2,14 +2,16 @@ use std::cell::Cell; use std::rc::Rc; use gpui::{ - canvas, div, px, Entity, InteractiveElement, ParentElement, Pixels, Render, ScrollHandle, - StatefulInteractiveElement as _, Styled, View, ViewContext, VisualContext, WindowContext, + div, px, size, Entity, InteractiveElement, ParentElement, Pixels, Render, ScrollHandle, + SharedString, Size, StatefulInteractiveElement as _, Styled, View, ViewContext, VisualContext, + WindowContext, }; use ui::button::Button; use ui::divider::Divider; +use ui::label::Label; use ui::scroll::{Scrollbar, ScrollbarAxis, ScrollbarState}; use ui::theme::ActiveTheme; -use ui::{h_flex, v_flex, StyledExt as _}; +use ui::{h_flex, v_flex, v_virtual_list, StyledExt as _}; pub struct ScrollableStory { focus_handle: gpui::FocusHandle, @@ -17,20 +19,33 @@ pub struct ScrollableStory { scroll_size: gpui::Size, scroll_state: Rc>, items: Vec, + item_sizes: Rc>>, test_width: Pixels, axis: ScrollbarAxis, + message: SharedString, } +const ITEM_HEIGHT: Pixels = px(30.); + impl ScrollableStory { fn new(cx: &mut ViewContext) -> Self { + let items = (0..5000).map(|i| format!("Item {}", i)).collect::>(); + let test_width = px(3000.); + let item_sizes = items + .iter() + .map(|_| size(test_width, ITEM_HEIGHT)) + .collect::>(); + Self { focus_handle: cx.focus_handle(), scroll_handle: ScrollHandle::new(), scroll_state: Rc::new(Cell::new(ScrollbarState::default())), scroll_size: gpui::Size::default(), - items: (0..500).map(|i| format!("Item {}", i)).collect::>(), - test_width: px(3000.), + items, + item_sizes: Rc::new(item_sizes), + test_width, axis: ScrollbarAxis::Both, + message: SharedString::default(), } } @@ -40,18 +55,27 @@ impl ScrollableStory { pub fn change_test_cases(&mut self, n: usize, cx: &mut ViewContext) { if n == 0 { - self.items = (0..500).map(|i| format!("Item {}", i)).collect::>(); + self.items = (0..5000).map(|i| format!("Item {}", i)).collect::>(); self.test_width = px(3000.); } else if n == 1 { self.items = (0..100).map(|i| format!("Item {}", i)).collect::>(); self.test_width = px(10000.); } else if n == 2 { - self.items = (0..500).map(|i| format!("Item {}", i)).collect::>(); + self.items = (0..500000) + .map(|i| format!("Item {}", i)) + .collect::>(); self.test_width = px(10000.); } else { self.items = (0..5).map(|i| format!("Item {}", i)).collect::>(); self.test_width = px(10000.); } + + self.item_sizes = self + .items + .iter() + .map(|_| size(self.test_width, ITEM_HEIGHT)) + .collect::>() + .into(); self.scroll_state.set(ScrollbarState::default()); cx.notify(); } @@ -60,6 +84,11 @@ impl ScrollableStory { self.axis = axis; cx.notify(); } + + fn set_message(&mut self, msg: &str, cx: &mut ViewContext) { + self.message = SharedString::from(msg.to_string()); + cx.notify(); + } } impl super::Story for ScrollableStory { @@ -68,7 +97,7 @@ impl super::Story for ScrollableStory { } fn description() -> &'static str { - "Add vertical or horizontal, or both scrollbars to a container." + "Add vertical or horizontal, or both scrollbars to a container, and use `virtual_list` to render a large number of items." } fn new_view(cx: &mut WindowContext) -> View { @@ -92,91 +121,117 @@ impl Render for ScrollableStory { .child( h_flex() .gap_2() - .child(Button::new("test-0").label("Size 0").on_click(cx.listener( - |view, _, cx| { - view.change_test_cases(0, cx); - }, - ))) - .child(Button::new("test-1").label("Size 1").on_click(cx.listener( - |view, _, cx| { - view.change_test_cases(1, cx); - }, - ))) - .child(Button::new("test-2").label("Size 2").on_click(cx.listener( - |view, _, cx| { - view.change_test_cases(2, cx); - }, - ))) - .child(Button::new("test-3").label("Size 3").on_click(cx.listener( - |view, _, cx| { - view.change_test_cases(3, cx); - }, - ))) - .child(Divider::vertical().px_2()) + .justify_between() .child( - Button::new("test-axis-both") - .label("Both Scrollbar") - .on_click( - cx.listener(|view, _, cx| { - view.change_axis(ScrollbarAxis::Both, cx) - }), - ), - ) - .child( - Button::new("test-axis-vertical") - .label("Vertical") - .on_click(cx.listener(|view, _, cx| { - view.change_axis(ScrollbarAxis::Vertical, cx) - })), - ) - .child( - Button::new("test-axis-horizontal") - .label("Horizontal") - .on_click(cx.listener(|view, _, cx| { - view.change_axis(ScrollbarAxis::Horizontal, cx) - })), - ), - ) - .child( - div() - .w_full() - .border_1() - .border_color(cx.theme().border) - .child( - div() - .relative() - .w_full() - .h(px(350.)) + h_flex() + .gap_2() + .child(Button::new("test-0").label("Size 0").on_click(cx.listener( + |view, _, cx| { + view.change_test_cases(0, cx); + }, + ))) + .child(Button::new("test-1").label("Size 1").on_click(cx.listener( + |view, _, cx| { + view.change_test_cases(1, cx); + }, + ))) + .child(Button::new("test-2").label("Size 2").on_click(cx.listener( + |view, _, cx| { + view.change_test_cases(2, cx); + }, + ))) + .child(Button::new("test-3").label("Size 3").on_click(cx.listener( + |view, _, cx| { + view.change_test_cases(3, cx); + }, + ))) + .child(Divider::vertical().px_2()) .child( - div() - .id("scroll-story") - .overflow_scroll() - .p_4() - .size_full() - .track_scroll(&self.scroll_handle) - .child( - v_flex() - .gap_1() - .w(self.test_width) - .children(self.items.iter().map(|s| { - div().bg(cx.theme().card).child(s.clone()) - })) - .child({ - let view = cx.view().clone(); - canvas( - move |bounds, cx| { - view.update(cx, |r, _| { - r.scroll_size = bounds.size - }) - }, - |_, _, _| {}, - ) - .absolute() - .size_full() - }), - ), + Button::new("test-axis-both") + .label("Both Scrollbar") + .on_click(cx.listener(|view, _, cx| { + view.change_axis(ScrollbarAxis::Both, cx) + })), ) .child( + Button::new("test-axis-vertical") + .label("Vertical") + .on_click(cx.listener(|view, _, cx| { + view.change_axis(ScrollbarAxis::Vertical, cx) + })), + ) + .child( + Button::new("test-axis-horizontal") + .label("Horizontal") + .on_click(cx.listener(|view, _, cx| { + view.change_axis(ScrollbarAxis::Horizontal, cx) + })), + ), + ) + .child(Label::new(self.message.clone())), + ) + .child( + div().w_full().child( + div().relative().w_full().h(px(350.)).child( + v_flex() + .id("test-0") + .relative() + .size_full() + .child( + v_virtual_list( + cx.view().clone(), + "items", + self.item_sizes.clone(), + move |story, visible_range, content_size, cx| { + story.set_message( + &format!("visible_range: {:?}", visible_range), + cx, + ); + story.scroll_size = content_size; + visible_range + .map(|ix| { + h_flex() + .h(ITEM_HEIGHT) + .gap_1() + .children( + (0..(story.test_width.0 as i32 / 100)) + .map(|i| { + div() + .flex() + .h_full() + .items_center() + .justify_center() + .text_sm() + .w(px(100.)) + .bg( + if cx.theme().mode.is_dark() + { + ui::gray_800() + } else { + ui::gray_100() + }, + ) + .child(if i == 0 { + format!("{}", ix) + } else { + format!("{}", i) + }) + }) + .collect::>(), + ) + .items_center() + }) + .collect::>() + }, + ) + .track_scroll(&self.scroll_handle) + .p_4() + .border_1() + .border_color(cx.theme().border) + .v_flex() + .gap_1(), + ) + .child({ div() .absolute() .top_0() @@ -191,14 +246,12 @@ impl Render for ScrollableStory { self.scroll_size, ) .axis(self.axis), - ), - ), + ) + }), ), + ), ) .child({ - let items = self.items.clone(); - let test_width = self.test_width; - div() .relative() .border_1() @@ -212,14 +265,18 @@ impl Render for ScrollableStory { .scrollable(cx.view().entity_id(), ScrollbarAxis::Vertical) .focusable() .p_3() - .w(test_width) + .w(self.test_width) .gap_1() .child("Hello world") - .children( - items - .iter() - .map(|s| div().bg(cx.theme().card).child(s.clone())), - ), + .children(self.items.iter().take(500).map(|item| { + div() + .h(ITEM_HEIGHT) + .bg(cx.theme().background) + .items_center() + .justify_center() + .text_sm() + .child(item.to_string()) + })), ) }) } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index dff8d1f4..ed60faa5 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -5,7 +5,6 @@ mod icon; mod root; mod styled; mod svg_img; -mod table_row; mod time; mod title_bar; @@ -47,6 +46,7 @@ pub mod tab; pub mod table; pub mod theme; pub mod tooltip; +pub mod virtual_list; pub mod webview; // re-export @@ -59,6 +59,7 @@ pub use root::{ContextModal, Root}; pub use styled::*; pub use time::*; pub use title_bar::*; +pub use virtual_list::{h_virtual_list, v_virtual_list, VirtualList}; pub use colors::*; pub use icon::*; diff --git a/crates/ui/src/table.rs b/crates/ui/src/table.rs index 5a9d1347..6cf7d8e7 100644 --- a/crates/ui/src/table.rs +++ b/crates/ui/src/table.rs @@ -5,12 +5,13 @@ use crate::{ h_flex, popup_menu::PopupMenu, scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState}, - table_row::table_row, theme::ActiveTheme, - v_flex, Icon, IconName, Sizable, Size, StyleSized as _, + v_flex, + virtual_list::virtual_list, + Icon, IconName, Sizable, Size, StyleSized as _, }; use gpui::{ - actions, canvas, div, prelude::FluentBuilder, px, uniform_list, AppContext, Bounds, Div, + actions, canvas, div, prelude::FluentBuilder, px, uniform_list, AppContext, Axis, Bounds, Div, DragMoveEvent, Edges, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyBinding, ListSizingBehavior, MouseButton, ParentElement, Pixels, Point, Render, ScrollHandle, ScrollStrategy, SharedString, Stateful, @@ -975,11 +976,11 @@ where let is_stripe_row = self.stripe && row_ix % 2 != 0; let is_selected = self.selected_row == Some(row_ix); let view = cx.view().clone(); - let col_groups: Rc> = Rc::new( + let col_sizes: Rc>> = Rc::new( self.col_groups .iter() .skip(left_cols_count) - .cloned() + .map(|col| col.bounds.size) .collect(), ); @@ -1030,13 +1031,9 @@ where .h_full() .overflow_hidden() .relative() - .child(table_row( - view, - row_ix, - col_groups, - self.horizontal_scroll_handle.clone(), - { - move |table, visible_range: Range, cx| { + .child( + virtual_list(view, row_ix, Axis::Horizontal, col_sizes, { + move |table, visible_range: Range, _, cx| { visible_range .map(|col_ix| { let col_ix = col_ix + left_cols_count; @@ -1048,8 +1045,9 @@ where }) .collect::>() } - }, - )) + }) + .with_scroll_handle(&self.horizontal_scroll_handle), + ) .child(self.delegate.render_last_empty_col(cx)), ) // Row selected style diff --git a/crates/ui/src/table_row.rs b/crates/ui/src/table_row.rs deleted file mode 100644 index 98d9a2a9..00000000 --- a/crates/ui/src/table_row.rs +++ /dev/null @@ -1,274 +0,0 @@ -//! Table row component for render a large number of differently sized columns (Must ensure each column width). -//! -//! Only visible columns are rendered for performance reasons. -//! -//! Inspired by uniform_list to rolate vertically to horizontally. -//! -//! https://github.com/zed-industries/zed/blob/0ae1603610ab6b265bdfbee7b8dbc23c5ab06edc/crates/gpui/src/elements/uniform_list.rs -use std::{cmp, ops::Range, rc::Rc}; - -use gpui::{ - div, point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Div, Element, ElementId, - Hitbox, InteractiveElement, IntoElement, IsZero as _, Pixels, Render, ScrollHandle, - SharedString, Size, Stateful, StyleRefinement, Styled, View, ViewContext, WindowContext, -}; -use smallvec::SmallVec; - -use crate::table::ColGroup; - -pub(crate) fn table_row( - view: View, - row_ix: usize, - col_groups: Rc>, - scroll_handle: ScrollHandle, - f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> Vec, -) -> TableRow -where - R: IntoElement, - V: Render, -{ - let id = ElementId::NamedInteger(SharedString::from("table-row"), row_ix); - - let render_range = move |range, cx: &mut WindowContext| { - view.update(cx, |this, cx| { - f(this, range, cx) - .into_iter() - .map(|component| component.into_any_element()) - .collect() - }) - }; - - TableRow { - id: id.clone(), - base: div().id(id).size_full(), - scroll_handle, - cols_count: col_groups.len(), - col_groups, - render_cols: Box::new(render_range), - } -} - -pub struct TableRow { - id: ElementId, - base: Stateful
, - scroll_handle: ScrollHandle, - // scroll_handle: ScrollHandle, - cols_count: usize, - col_groups: Rc>, - render_cols: - Box Fn(Range, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>, -} - -impl Styled for TableRow { - fn style(&mut self) -> &mut StyleRefinement { - self.base.style() - } -} - -/// Frame state used by the [TableRow]. -pub struct TableRowFrameState { - cols: SmallVec<[AnyElement; 32]>, - // decorations: SmallVec<[AnyElement; 1]>, -} - -impl TableRow { - #[allow(dead_code)] - fn measure_col(&self, cx: &mut WindowContext) -> Size { - if self.cols_count == 0 { - return Size::default(); - } - - let col_ix = self.cols_count - 1; - let mut items = (self.render_cols)(col_ix..col_ix + 1, cx); - let Some(mut item_to_measure) = items.pop() else { - return Size::default(); - }; - - let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - item_to_measure.layout_as_root(available_space, cx) - } -} - -impl IntoElement for TableRow { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Element for TableRow { - type RequestLayoutState = TableRowFrameState; - type PrepaintState = Option; - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn request_layout( - &mut self, - global_id: Option<&gpui::GlobalElementId>, - cx: &mut WindowContext, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - let (layout_id, _) = self.base.request_layout(global_id, cx); - - ( - layout_id, - TableRowFrameState { - cols: SmallVec::new(), - }, - ) - } - - fn prepaint( - &mut self, - global_id: Option<&gpui::GlobalElementId>, - bounds: gpui::Bounds, - frame_state: &mut Self::RequestLayoutState, - cx: &mut WindowContext, - ) -> Self::PrepaintState { - let style = self.base.interactivity().compute_style(global_id, None, cx); - let border = style.border_widths.to_pixels(cx.rem_size()); - let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); - - let padded_bounds = Bounds::from_corners( - bounds.origin + point(border.left + padding.left, border.top + padding.top), - bounds.lower_right() - - point(border.right + padding.right, border.bottom + padding.bottom), - ); - - // This is important to get the width of each column to measure the visible columns. - // - // So the col must have a width. - let col_widths = self - .col_groups - .iter() - .map(|col| col.width.0) - .collect::>(); - - let content_height = padded_bounds.size.height; - let content_width = px(col_widths.iter().sum::()); - let content_size = Size { - width: content_width, - height: content_height, - }; - - self.base.interactivity().prepaint( - global_id, - bounds, - content_size, - cx, - |style, _, hitbox, cx| { - let mut scroll_offset = self.scroll_handle.offset(); - // dbg!(&scroll_offset); - let border = style.border_widths.to_pixels(cx.rem_size()); - let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); - - let padded_bounds = Bounds::from_corners( - bounds.origin + point(border.left + padding.left, border.top), - bounds.lower_right() - point(border.right + padding.right, border.bottom), - ); - - if self.cols_count > 0 { - let is_scrolled_horizontally = !scroll_offset.x.is_zero(); - let min_horizontal_scroll_offset = padded_bounds.size.width - content_width; - if is_scrolled_horizontally && scroll_offset.x < min_horizontal_scroll_offset { - scroll_offset.x = min_horizontal_scroll_offset; - } - scroll_offset.y = Pixels::ZERO; - - // Calculate the first and last visible element indices. - let mut cumulative_width = 0.0; - let mut first_visible_element_ix = 0; - for (i, &width) in col_widths.iter().enumerate() { - cumulative_width += width; - if cumulative_width > -(scroll_offset.x + padding.left).0 { - first_visible_element_ix = i; - break; - } - } - - cumulative_width = 0.0; - let mut last_visible_element_ix = 0; - for (i, &width) in col_widths.iter().enumerate() { - cumulative_width += width; - if cumulative_width > (-scroll_offset.x + padded_bounds.size.width).0 { - last_visible_element_ix = i + 1; - break; - } - } - if last_visible_element_ix == 0 { - last_visible_element_ix = self.cols_count; - } else { - last_visible_element_ix += 1; - } - let visible_range = first_visible_element_ix - ..cmp::min(last_visible_element_ix, self.cols_count); - - let items = (self.render_cols)(visible_range.clone(), cx); - - let content_mask = ContentMask { bounds }; - cx.with_content_mask(Some(content_mask), |cx| { - for (mut item, ix) in items.into_iter().zip(visible_range.clone()) { - let item_x = px(col_widths.iter().take(ix).sum::()); - - let item_origin = padded_bounds.origin - + point(item_x + scroll_offset.x + padding.left, padding.top); - // println!("{}, {}", item_origin.x, item_origin.y); - let available_height = padded_bounds.size.height; - let col_width = col_widths[ix]; - let available_space = size( - AvailableSpace::Definite(px(col_width)), - AvailableSpace::Definite(available_height), - ); - item.layout_as_root(available_space, cx); - item.prepaint_at(item_origin, cx); - frame_state.cols.push(item); - } - - // let bounds = Bounds::new( - // padded_bounds.origin - // + point(scroll_offset.x + padding.left, scroll_offset.y), - // padded_bounds.size, - // ); - // for decoration in &self.decorations { - // let mut decoration = decoration.as_ref().compute( - // visible_range.clone(), - // bounds, - // item_height, - // self.item_count, - // cx, - // ); - // let available_space = size( - // AvailableSpace::Definite(bounds.size.width), - // AvailableSpace::Definite(bounds.size.height), - // ); - // decoration.layout_as_root(available_space, cx); - // decoration.prepaint_at(bounds.origin, cx); - // frame_state.decorations.push(decoration); - // } - }); - } - - hitbox - }, - ) - } - - fn paint( - &mut self, - global_id: Option<&gpui::GlobalElementId>, - bounds: gpui::Bounds, - request_layout: &mut Self::RequestLayoutState, - hitbox: &mut Self::PrepaintState, - cx: &mut WindowContext, - ) { - self.base - .interactivity() - .paint(global_id, bounds, hitbox.as_ref(), cx, |_, cx| { - for col in &mut request_layout.cols { - col.paint(cx); - } - }) - } -} diff --git a/crates/ui/src/virtual_list.rs b/crates/ui/src/virtual_list.rs new file mode 100644 index 00000000..5b46a6a0 --- /dev/null +++ b/crates/ui/src/virtual_list.rs @@ -0,0 +1,448 @@ +//! 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::{cmp, ops::Range, rc::Rc}; + +use gpui::{ + div, point, px, size, AnyElement, AvailableSpace, Axis, Bounds, ContentMask, Div, Element, + ElementId, GlobalElementId, Hitbox, InteractiveElement, IntoElement, IsZero as _, Pixels, + Render, ScrollHandle, Size, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, + View, ViewContext, WindowContext, +}; +use smallvec::SmallVec; + +/// Create a virtual list in Vertical direction. +/// +/// This is like `uniform_list` in GPUI, but support two axis. +/// +/// The `item_sizes` is the size of each column. +pub fn v_virtual_list( + view: View, + id: impl Into, + item_sizes: Rc>>, + f: impl 'static + Fn(&mut V, Range, Size, &mut ViewContext) -> Vec, +) -> VirtualList +where + R: IntoElement, + V: Render, +{ + virtual_list(view, id, Axis::Vertical, item_sizes, f) +} + +/// Create a virtual list in Horizontal direction. +pub fn h_virtual_list( + view: View, + id: impl Into, + item_sizes: Rc>>, + f: impl 'static + Fn(&mut V, Range, Size, &mut ViewContext) -> Vec, +) -> VirtualList +where + R: IntoElement, + V: Render, +{ + virtual_list(view, id, Axis::Horizontal, item_sizes, f) +} + +pub(crate) fn virtual_list( + view: View, + id: impl Into, + axis: Axis, + item_sizes: Rc>>, + f: impl 'static + Fn(&mut V, Range, Size, &mut ViewContext) -> Vec, +) -> VirtualList +where + R: IntoElement, + V: Render, +{ + let id: ElementId = id.into(); + let scroll_handle = ScrollHandle::default(); + let render_range = move |visible_range, content_size, cx: &mut WindowContext| { + view.update(cx, |this, cx| { + f(this, visible_range, content_size, 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), + } +} + +/// VirtualItem component for rendering a large number of differently sized columns. +pub struct VirtualList { + id: ElementId, + axis: Axis, + base: Stateful
, + scroll_handle: ScrollHandle, + // scroll_handle: ScrollHandle, + items_count: usize, + item_sizes: Rc>>, + render_items: Box< + dyn for<'a> Fn( + Range, + Size, + &'a mut WindowContext, + ) -> SmallVec<[AnyElement; 64]>, + >, +} + +impl Styled for VirtualList { + fn style(&mut self) -> &mut StyleRefinement { + self.base.style() + } +} + +impl VirtualList { + pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { + self.base = self.base.track_scroll(&scroll_handle); + self.scroll_handle = scroll_handle.clone(); + self + } + + /// Specify for table. + pub(crate) fn with_scroll_handle(mut self, scroll_handle: &ScrollHandle) -> Self { + self.base = div().id(self.id.clone()).size_full(); + self.scroll_handle = scroll_handle.clone(); + self + } + + /// Measure first item to get the size. + fn measure_item(&self, cx: &mut WindowContext) -> Size { + if self.items_count == 0 { + return Size::default(); + } + + let item_ix = 0; + let mut items = (self.render_items)(item_ix..item_ix + 1, Size::default(), cx); + let Some(mut item_to_measure) = items.pop() else { + return Size::default(); + }; + let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); + item_to_measure.layout_as_root(available_space, cx) + } +} + +/// Frame state used by the [VirtualItem]. +pub struct VirtualListFrameState { + /// Visible items to be painted. + items: SmallVec<[AnyElement; 32]>, + item_sizes: Vec, + item_origins: Vec, +} + +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 request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { + let style = self.base.interactivity().compute_style(global_id, None, cx); + let font_size = cx.text_style().font_size.to_pixels(cx.rem_size()); + + // Including the gap between items for calculate the item size + let gap = match self.axis { + Axis::Horizontal => style.gap.width, + Axis::Vertical => style.gap.height, + } + .to_pixels(font_size.into(), cx.rem_size()); + + // TODO: To cache the item_sizes, item_origins + // If there have 500,000 items, this method will speed about 500~600µs + // let start = std::time::Instant::now(); + // Prepare each item's size by axis + let item_sizes = match self.axis { + Axis::Horizontal => self + .item_sizes + .iter() + .enumerate() + .map(|(i, size)| { + if i == self.items_count - 1 { + size.width + } else { + size.width + gap + } + }) + .collect::>(), + Axis::Vertical => self + .item_sizes + .iter() + .enumerate() + .map(|(i, size)| { + if i == self.items_count - 1 { + size.height + } else { + size.height + gap + } + }) + .collect::>(), + }; + + // Prepare each item's origin by axis + let item_origins = match self.axis { + Axis::Horizontal => item_sizes + .iter() + .scan(px(0.), |cumulative_x, size| { + let x = *cumulative_x; + *cumulative_x += *size; + Some(x) + }) + .collect::>(), + Axis::Vertical => item_sizes + .iter() + .scan(px(0.), |cumulative_y, size| { + let y = *cumulative_y; + *cumulative_y += *size; + Some(y) + }) + .collect::>(), + }; + // println!("layout: {} {:?}", item_sizes.len(), start.elapsed()); + + let (layout_id, _) = self.base.request_layout(global_id, cx); + + ( + layout_id, + VirtualListFrameState { + items: SmallVec::new(), + item_sizes, + item_origins, + }, + ) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + layout: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> Self::PrepaintState { + let style = self.base.interactivity().compute_style(global_id, None, cx); + let border = style.border_widths.to_pixels(cx.rem_size()); + let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); + + let first_item_size = self.measure_item(cx); + + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top + padding.top), + bounds.lower_right() + - point(border.right + padding.right, border.bottom + padding.bottom), + ); + + // Get border + padding pixel size + let padding_size = match self.axis { + Axis::Horizontal => border.left + padding.left + border.right + padding.right, + Axis::Vertical => border.top + padding.top + border.bottom + padding.bottom, + }; + + let item_sizes = &layout.item_sizes; + let item_origins = &layout.item_origins; + + let content_size = match self.axis { + Axis::Horizontal => Size { + width: px(item_sizes.iter().map(|size| size.0).sum::()) + padding_size, + height: (first_item_size.height + padding_size).max(padded_bounds.size.height), + }, + Axis::Vertical => Size { + width: (first_item_size.width + padding_size).max(padded_bounds.size.width), + height: px(item_sizes.iter().map(|size| size.0).sum::()) + padding_size, + }, + }; + + self.base.interactivity().prepaint( + global_id, + bounds, + content_size, + cx, + |style, _, hitbox, cx| { + let mut scroll_offset = self.scroll_handle.offset(); + let border = style.border_widths.to_pixels(cx.rem_size()); + let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); + + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top), + bounds.lower_right() - point(border.right + padding.right, border.bottom), + ); + + if self.items_count > 0 { + let is_scrolled = match self.axis { + Axis::Horizontal => !scroll_offset.x.is_zero(), + Axis::Vertical => !scroll_offset.y.is_zero(), + }; + + let min_scroll_offset = match self.axis { + Axis::Horizontal => padded_bounds.size.width - content_size.width, + Axis::Vertical => padded_bounds.size.height - content_size.height, + }; + + 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 + padding.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 + padded_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 + padding.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 + padded_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(), content_size, cx); + + let content_mask = ContentMask { bounds }; + cx.with_content_mask(Some(content_mask), |cx| { + for (mut item, ix) in items.into_iter().zip(visible_range.clone()) { + let item_origin = match self.axis { + Axis::Horizontal => { + padded_bounds.origin + + point( + item_origins[ix] + scroll_offset.x, + padding.top + scroll_offset.y, + ) + } + Axis::Vertical => { + padded_bounds.origin + + point( + scroll_offset.x, + padding.top + item_origins[ix] + scroll_offset.y, + ) + } + }; + + let available_space = match self.axis { + Axis::Horizontal => size( + AvailableSpace::Definite(item_sizes[ix]), + AvailableSpace::Definite(padded_bounds.size.height), + ), + Axis::Vertical => size( + AvailableSpace::Definite(padded_bounds.size.width), + AvailableSpace::Definite(item_sizes[ix]), + ), + }; + + item.layout_as_root(available_space, cx); + item.prepaint_at(item_origin, cx); + layout.items.push(item); + } + }); + } + + hitbox + }, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + layout: &mut Self::RequestLayoutState, + hitbox: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + self.base + .interactivity() + .paint(global_id, bounds, hitbox.as_ref(), cx, |_, cx| { + for item in &mut layout.items { + item.paint(cx); + } + }) + } +}