Files
Ely-GPUI-Components/src/lists/long.rs
T

146 lines
4.3 KiB
Rust

use std::rc::Rc;
use gpui::{
AnyElement, App, Div, ElementId, IntoElement, ListAlignment, ListState, ParentElement,
RenderOnce, StyleRefinement, Styled, Window, div, list,
};
use smallvec::SmallVec;
use crate::{motion::Spinner, primitives::IntersectionObserver, theme::ActiveTheme};
type Row = Rc<dyn Fn(usize, &mut Window, &mut App) -> AnyElement>;
/// A long list that builds only the rows in view, each as tall as it needs. `row(ix)` builds row `ix`. Rows added or dropped at the end keep the scroll. Give it a height.
#[derive(IntoElement)]
pub struct VirtualList {
id: ElementId,
base: Div,
count: usize,
row: Row,
}
impl VirtualList {
pub fn new(
id: impl Into<ElementId>,
count: usize,
row: impl Fn(usize, &mut Window, &mut App) -> AnyElement + 'static,
) -> Self {
Self {
id: id.into(),
base: div(),
count,
row: Rc::new(row),
}
}
}
impl Styled for VirtualList {
fn style(&mut self) -> &mut StyleRefinement {
self.base.style()
}
}
impl RenderOnce for VirtualList {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let (count, overdraw) = (
self.count,
cx.theme().list_overdraw().to_pixels(window.rem_size()),
);
let state = window
.use_keyed_state(self.id.clone(), cx, |_, _| {
ListState::new(count, ListAlignment::Top, overdraw)
})
.read(cx)
.clone();
let known = state.item_count();
if known != count {
log::debug!("virtual list {:?}: {known} rows to {count}", self.id);
if count > known {
state.splice(known..known, count - known);
} else {
state.splice(count..known, 0);
}
}
let row = self.row;
self.base
.child(list(state, move |ix, window, cx| row(ix, window, cx)).size_full())
}
}
type OnMore = Rc<dyn Fn(&mut Window, &mut App)>;
/// Rows that ask for more when their end comes into view: `on_more` runs once each time it arrives, a spinner shows while `loading`, and `done` ends the list quietly. Put it in a scrolling box.
#[derive(IntoElement)]
pub struct InfiniteList {
id: ElementId,
rows: SmallVec<[AnyElement; 8]>,
loading: bool,
done: bool,
on_more: Option<OnMore>,
}
impl InfiniteList {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
rows: SmallVec::new(),
loading: false,
done: false,
on_more: None,
}
}
pub fn loading(mut self, loading: bool) -> Self {
self.loading = loading;
self
}
/// Nothing more to load.
pub fn done(mut self, done: bool) -> Self {
self.done = done;
self
}
pub fn on_more(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_more = Some(Rc::new(handler));
self
}
}
impl ParentElement for InfiniteList {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.rows.extend(elements);
}
}
impl RenderOnce for InfiniteList {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let count = self.rows.len();
let end = match (self.done, self.loading) {
(true, _) => None,
(false, true) => Some(
div()
.flex()
.justify_center()
.py_3()
.child(Spinner::new((self.id.clone(), "spinner")))
.into_any_element(),
),
(false, false) => {
let (id, on_more) = (self.id.clone(), self.on_more);
Some(
IntersectionObserver::new((self.id, format!("end-{count}")), move |seen, window, cx| {
if seen && let Some(on_more) = &on_more {
log::info!("infinite list {id:?}: the end came into view after {count} rows");
on_more(window, cx);
}
})
.h_1()
.into_any_element(),
)
}
};
div().flex().flex_col().children(self.rows).children(end)
}
}