virtual_list: Add VirtualList (#495)
Vistual List for render a large number of differently sized rows/columns. > NOTE: This must ensure each column width or row height. 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.
This commit is contained in:
parent
8260b5f6a0
commit
4f7ca4bb9a
5 changed files with 619 additions and 389 deletions
|
|
@ -2,14 +2,16 @@ use std::cell::Cell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
canvas, div, px, Entity, InteractiveElement, ParentElement, Pixels, Render, ScrollHandle,
|
div, px, size, Entity, InteractiveElement, ParentElement, Pixels, Render, ScrollHandle,
|
||||||
StatefulInteractiveElement as _, Styled, View, ViewContext, VisualContext, WindowContext,
|
SharedString, Size, StatefulInteractiveElement as _, Styled, View, ViewContext, VisualContext,
|
||||||
|
WindowContext,
|
||||||
};
|
};
|
||||||
use ui::button::Button;
|
use ui::button::Button;
|
||||||
use ui::divider::Divider;
|
use ui::divider::Divider;
|
||||||
|
use ui::label::Label;
|
||||||
use ui::scroll::{Scrollbar, ScrollbarAxis, ScrollbarState};
|
use ui::scroll::{Scrollbar, ScrollbarAxis, ScrollbarState};
|
||||||
use ui::theme::ActiveTheme;
|
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 {
|
pub struct ScrollableStory {
|
||||||
focus_handle: gpui::FocusHandle,
|
focus_handle: gpui::FocusHandle,
|
||||||
|
|
@ -17,20 +19,33 @@ pub struct ScrollableStory {
|
||||||
scroll_size: gpui::Size<Pixels>,
|
scroll_size: gpui::Size<Pixels>,
|
||||||
scroll_state: Rc<Cell<ScrollbarState>>,
|
scroll_state: Rc<Cell<ScrollbarState>>,
|
||||||
items: Vec<String>,
|
items: Vec<String>,
|
||||||
|
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||||
test_width: Pixels,
|
test_width: Pixels,
|
||||||
axis: ScrollbarAxis,
|
axis: ScrollbarAxis,
|
||||||
|
message: SharedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ITEM_HEIGHT: Pixels = px(30.);
|
||||||
|
|
||||||
impl ScrollableStory {
|
impl ScrollableStory {
|
||||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
let items = (0..5000).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
|
||||||
|
let test_width = px(3000.);
|
||||||
|
let item_sizes = items
|
||||||
|
.iter()
|
||||||
|
.map(|_| size(test_width, ITEM_HEIGHT))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
scroll_handle: ScrollHandle::new(),
|
scroll_handle: ScrollHandle::new(),
|
||||||
scroll_state: Rc::new(Cell::new(ScrollbarState::default())),
|
scroll_state: Rc::new(Cell::new(ScrollbarState::default())),
|
||||||
scroll_size: gpui::Size::default(),
|
scroll_size: gpui::Size::default(),
|
||||||
items: (0..500).map(|i| format!("Item {}", i)).collect::<Vec<_>>(),
|
items,
|
||||||
test_width: px(3000.),
|
item_sizes: Rc::new(item_sizes),
|
||||||
|
test_width,
|
||||||
axis: ScrollbarAxis::Both,
|
axis: ScrollbarAxis::Both,
|
||||||
|
message: SharedString::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,18 +55,27 @@ impl ScrollableStory {
|
||||||
|
|
||||||
pub fn change_test_cases(&mut self, n: usize, cx: &mut ViewContext<Self>) {
|
pub fn change_test_cases(&mut self, n: usize, cx: &mut ViewContext<Self>) {
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
self.items = (0..500).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
|
self.items = (0..5000).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
|
||||||
self.test_width = px(3000.);
|
self.test_width = px(3000.);
|
||||||
} else if n == 1 {
|
} else if n == 1 {
|
||||||
self.items = (0..100).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
|
self.items = (0..100).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
|
||||||
self.test_width = px(10000.);
|
self.test_width = px(10000.);
|
||||||
} else if n == 2 {
|
} else if n == 2 {
|
||||||
self.items = (0..500).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
|
self.items = (0..500000)
|
||||||
|
.map(|i| format!("Item {}", i))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
self.test_width = px(10000.);
|
self.test_width = px(10000.);
|
||||||
} else {
|
} else {
|
||||||
self.items = (0..5).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
|
self.items = (0..5).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
|
||||||
self.test_width = px(10000.);
|
self.test_width = px(10000.);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.item_sizes = self
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|_| size(self.test_width, ITEM_HEIGHT))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into();
|
||||||
self.scroll_state.set(ScrollbarState::default());
|
self.scroll_state.set(ScrollbarState::default());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
@ -60,6 +84,11 @@ impl ScrollableStory {
|
||||||
self.axis = axis;
|
self.axis = axis;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_message(&mut self, msg: &str, cx: &mut ViewContext<Self>) {
|
||||||
|
self.message = SharedString::from(msg.to_string());
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl super::Story for ScrollableStory {
|
impl super::Story for ScrollableStory {
|
||||||
|
|
@ -68,7 +97,7 @@ impl super::Story for ScrollableStory {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description() -> &'static str {
|
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<impl gpui::FocusableView> {
|
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||||
|
|
@ -92,91 +121,117 @@ impl Render for ScrollableStory {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(Button::new("test-0").label("Size 0").on_click(cx.listener(
|
.justify_between()
|
||||||
|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(
|
.child(
|
||||||
Button::new("test-axis-both")
|
h_flex()
|
||||||
.label("Both Scrollbar")
|
.gap_2()
|
||||||
.on_click(
|
.child(Button::new("test-0").label("Size 0").on_click(cx.listener(
|
||||||
cx.listener(|view, _, cx| {
|
|view, _, cx| {
|
||||||
view.change_axis(ScrollbarAxis::Both, cx)
|
view.change_test_cases(0, cx);
|
||||||
}),
|
},
|
||||||
),
|
)))
|
||||||
)
|
.child(Button::new("test-1").label("Size 1").on_click(cx.listener(
|
||||||
.child(
|
|view, _, cx| {
|
||||||
Button::new("test-axis-vertical")
|
view.change_test_cases(1, cx);
|
||||||
.label("Vertical")
|
},
|
||||||
.on_click(cx.listener(|view, _, cx| {
|
)))
|
||||||
view.change_axis(ScrollbarAxis::Vertical, 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-axis-horizontal")
|
)))
|
||||||
.label("Horizontal")
|
.child(Button::new("test-3").label("Size 3").on_click(cx.listener(
|
||||||
.on_click(cx.listener(|view, _, cx| {
|
|view, _, cx| {
|
||||||
view.change_axis(ScrollbarAxis::Horizontal, cx)
|
view.change_test_cases(3, cx);
|
||||||
})),
|
},
|
||||||
),
|
)))
|
||||||
)
|
.child(Divider::vertical().px_2())
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w_full()
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.relative()
|
|
||||||
.w_full()
|
|
||||||
.h(px(350.))
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
Button::new("test-axis-both")
|
||||||
.id("scroll-story")
|
.label("Both Scrollbar")
|
||||||
.overflow_scroll()
|
.on_click(cx.listener(|view, _, cx| {
|
||||||
.p_4()
|
view.change_axis(ScrollbarAxis::Both, cx)
|
||||||
.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()
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.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::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.items_center()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.track_scroll(&self.scroll_handle)
|
||||||
|
.p_4()
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.v_flex()
|
||||||
|
.gap_1(),
|
||||||
|
)
|
||||||
|
.child({
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
.top_0()
|
.top_0()
|
||||||
|
|
@ -191,14 +246,12 @@ impl Render for ScrollableStory {
|
||||||
self.scroll_size,
|
self.scroll_size,
|
||||||
)
|
)
|
||||||
.axis(self.axis),
|
.axis(self.axis),
|
||||||
),
|
)
|
||||||
),
|
}),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child({
|
.child({
|
||||||
let items = self.items.clone();
|
|
||||||
let test_width = self.test_width;
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
.border_1()
|
.border_1()
|
||||||
|
|
@ -212,14 +265,18 @@ impl Render for ScrollableStory {
|
||||||
.scrollable(cx.view().entity_id(), ScrollbarAxis::Vertical)
|
.scrollable(cx.view().entity_id(), ScrollbarAxis::Vertical)
|
||||||
.focusable()
|
.focusable()
|
||||||
.p_3()
|
.p_3()
|
||||||
.w(test_width)
|
.w(self.test_width)
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child("Hello world")
|
.child("Hello world")
|
||||||
.children(
|
.children(self.items.iter().take(500).map(|item| {
|
||||||
items
|
div()
|
||||||
.iter()
|
.h(ITEM_HEIGHT)
|
||||||
.map(|s| div().bg(cx.theme().card).child(s.clone())),
|
.bg(cx.theme().background)
|
||||||
),
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_sm()
|
||||||
|
.child(item.to_string())
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ mod icon;
|
||||||
mod root;
|
mod root;
|
||||||
mod styled;
|
mod styled;
|
||||||
mod svg_img;
|
mod svg_img;
|
||||||
mod table_row;
|
|
||||||
mod time;
|
mod time;
|
||||||
mod title_bar;
|
mod title_bar;
|
||||||
|
|
||||||
|
|
@ -47,6 +46,7 @@ pub mod tab;
|
||||||
pub mod table;
|
pub mod table;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
pub mod tooltip;
|
pub mod tooltip;
|
||||||
|
pub mod virtual_list;
|
||||||
pub mod webview;
|
pub mod webview;
|
||||||
|
|
||||||
// re-export
|
// re-export
|
||||||
|
|
@ -59,6 +59,7 @@ pub use root::{ContextModal, Root};
|
||||||
pub use styled::*;
|
pub use styled::*;
|
||||||
pub use time::*;
|
pub use time::*;
|
||||||
pub use title_bar::*;
|
pub use title_bar::*;
|
||||||
|
pub use virtual_list::{h_virtual_list, v_virtual_list, VirtualList};
|
||||||
|
|
||||||
pub use colors::*;
|
pub use colors::*;
|
||||||
pub use icon::*;
|
pub use icon::*;
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@ use crate::{
|
||||||
h_flex,
|
h_flex,
|
||||||
popup_menu::PopupMenu,
|
popup_menu::PopupMenu,
|
||||||
scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState},
|
scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState},
|
||||||
table_row::table_row,
|
|
||||||
theme::ActiveTheme,
|
theme::ActiveTheme,
|
||||||
v_flex, Icon, IconName, Sizable, Size, StyleSized as _,
|
v_flex,
|
||||||
|
virtual_list::virtual_list,
|
||||||
|
Icon, IconName, Sizable, Size, StyleSized as _,
|
||||||
};
|
};
|
||||||
use gpui::{
|
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,
|
DragMoveEvent, Edges, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
|
||||||
InteractiveElement, IntoElement, KeyBinding, ListSizingBehavior, MouseButton, ParentElement,
|
InteractiveElement, IntoElement, KeyBinding, ListSizingBehavior, MouseButton, ParentElement,
|
||||||
Pixels, Point, Render, ScrollHandle, ScrollStrategy, SharedString, Stateful,
|
Pixels, Point, Render, ScrollHandle, ScrollStrategy, SharedString, Stateful,
|
||||||
|
|
@ -975,11 +976,11 @@ where
|
||||||
let is_stripe_row = self.stripe && row_ix % 2 != 0;
|
let is_stripe_row = self.stripe && row_ix % 2 != 0;
|
||||||
let is_selected = self.selected_row == Some(row_ix);
|
let is_selected = self.selected_row == Some(row_ix);
|
||||||
let view = cx.view().clone();
|
let view = cx.view().clone();
|
||||||
let col_groups: Rc<Vec<ColGroup>> = Rc::new(
|
let col_sizes: Rc<Vec<gpui::Size<Pixels>>> = Rc::new(
|
||||||
self.col_groups
|
self.col_groups
|
||||||
.iter()
|
.iter()
|
||||||
.skip(left_cols_count)
|
.skip(left_cols_count)
|
||||||
.cloned()
|
.map(|col| col.bounds.size)
|
||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1030,13 +1031,9 @@ where
|
||||||
.h_full()
|
.h_full()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.relative()
|
.relative()
|
||||||
.child(table_row(
|
.child(
|
||||||
view,
|
virtual_list(view, row_ix, Axis::Horizontal, col_sizes, {
|
||||||
row_ix,
|
move |table, visible_range: Range<usize>, _, cx| {
|
||||||
col_groups,
|
|
||||||
self.horizontal_scroll_handle.clone(),
|
|
||||||
{
|
|
||||||
move |table, visible_range: Range<usize>, cx| {
|
|
||||||
visible_range
|
visible_range
|
||||||
.map(|col_ix| {
|
.map(|col_ix| {
|
||||||
let col_ix = col_ix + left_cols_count;
|
let col_ix = col_ix + left_cols_count;
|
||||||
|
|
@ -1048,8 +1045,9 @@ where
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
))
|
.with_scroll_handle(&self.horizontal_scroll_handle),
|
||||||
|
)
|
||||||
.child(self.delegate.render_last_empty_col(cx)),
|
.child(self.delegate.render_last_empty_col(cx)),
|
||||||
)
|
)
|
||||||
// Row selected style
|
// Row selected style
|
||||||
|
|
|
||||||
|
|
@ -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<R, V>(
|
|
||||||
view: View<V>,
|
|
||||||
row_ix: usize,
|
|
||||||
col_groups: Rc<Vec<ColGroup>>,
|
|
||||||
scroll_handle: ScrollHandle,
|
|
||||||
f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<R>,
|
|
||||||
) -> 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<Div>,
|
|
||||||
scroll_handle: ScrollHandle,
|
|
||||||
// scroll_handle: ScrollHandle,
|
|
||||||
cols_count: usize,
|
|
||||||
col_groups: Rc<Vec<ColGroup>>,
|
|
||||||
render_cols:
|
|
||||||
Box<dyn for<'a> Fn(Range<usize>, &'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<Pixels> {
|
|
||||||
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<Hitbox>;
|
|
||||||
|
|
||||||
fn id(&self) -> Option<gpui::ElementId> {
|
|
||||||
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<gpui::Pixels>,
|
|
||||||
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::<Vec<_>>();
|
|
||||||
|
|
||||||
let content_height = padded_bounds.size.height;
|
|
||||||
let content_width = px(col_widths.iter().sum::<f32>());
|
|
||||||
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::<f32>());
|
|
||||||
|
|
||||||
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<gpui::Pixels>,
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
448
crates/ui/src/virtual_list.rs
Normal file
448
crates/ui/src/virtual_list.rs
Normal file
|
|
@ -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<R, V>(
|
||||||
|
view: View<V>,
|
||||||
|
id: impl Into<ElementId>,
|
||||||
|
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||||
|
f: impl 'static + Fn(&mut V, Range<usize>, Size<Pixels>, &mut ViewContext<V>) -> Vec<R>,
|
||||||
|
) -> 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<R, V>(
|
||||||
|
view: View<V>,
|
||||||
|
id: impl Into<ElementId>,
|
||||||
|
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||||
|
f: impl 'static + Fn(&mut V, Range<usize>, Size<Pixels>, &mut ViewContext<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: View<V>,
|
||||||
|
id: impl Into<ElementId>,
|
||||||
|
axis: Axis,
|
||||||
|
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||||
|
f: impl 'static + Fn(&mut V, Range<usize>, Size<Pixels>, &mut ViewContext<V>) -> Vec<R>,
|
||||||
|
) -> 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<Div>,
|
||||||
|
scroll_handle: ScrollHandle,
|
||||||
|
// scroll_handle: ScrollHandle,
|
||||||
|
items_count: usize,
|
||||||
|
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||||
|
render_items: Box<
|
||||||
|
dyn for<'a> Fn(
|
||||||
|
Range<usize>,
|
||||||
|
Size<Pixels>,
|
||||||
|
&'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<Pixels> {
|
||||||
|
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<Pixels>,
|
||||||
|
item_origins: Vec<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 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::<Vec<_>>(),
|
||||||
|
Axis::Vertical => self
|
||||||
|
.item_sizes
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, size)| {
|
||||||
|
if i == self.items_count - 1 {
|
||||||
|
size.height
|
||||||
|
} else {
|
||||||
|
size.height + gap
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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::<Vec<_>>(),
|
||||||
|
Axis::Vertical => item_sizes
|
||||||
|
.iter()
|
||||||
|
.scan(px(0.), |cumulative_y, size| {
|
||||||
|
let y = *cumulative_y;
|
||||||
|
*cumulative_y += *size;
|
||||||
|
Some(y)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
};
|
||||||
|
// 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<Pixels>,
|
||||||
|
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::<f32>()) + 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::<f32>()) + 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<Pixels>,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue