use std::rc::Rc;
use gpui_kit::{
AnyElement, App, ElementId, InteractiveElement as _, IntoElement, ListSizingBehavior,
ListState, ParentElement, Pixels, RenderOnce, ScrollHandle, StatefulInteractiveElement as _,
StyleRefinement, Styled, Window,
base::{self, ScrollBounce, ScrollbarHandle, StyledExt as _, TestSupportExt as _},
div, list,
};
type RenderItem = Box<dyn FnMut(usize, &mut Window, &mut App) -> AnyElement>;
enum Content {
Children(Vec<AnyElement>),
List {
state: ListState,
row_hint: Pixels,
render_item: RenderItem,
},
}
#[derive(IntoElement)]
pub struct ScrollArea {
id: ElementId,
content: Content,
style: StyleRefinement,
}
impl ScrollArea {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
content: Content::Children(Vec::new()),
style: StyleRefinement::default(),
}
}
pub fn list(
id: impl Into<ElementId>,
state: &ListState,
row_hint: Pixels,
render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static,
) -> Self {
Self {
id: id.into(),
content: Content::List {
state: state.clone(),
row_hint,
render_item: Box::new(render_item),
},
style: StyleRefinement::default(),
}
}
}
impl Styled for ScrollArea {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl ParentElement for ScrollArea {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
if let Content::Children(children) = &mut self.content {
children.extend(elements);
}
}
}
fn chrome<H: ScrollbarHandle + Clone>(
id: &ElementId,
handle: &H,
viewport: impl IntoElement,
) -> (AnyElement, AnyElement, Rc<dyn Fn() -> bool>) {
let bounce = ScrollBounce::new(
ElementId::NamedChild(id.clone().into(), "bounce".into()),
handle,
viewport,
)
.enabled(true);
let scrollbar = base::Scrollbar::vertical(handle)
.id(ElementId::NamedChild(id.clone().into(), "scrollbar".into()));
let can_scroll = {
let handle = handle.clone();
move || handle.content_size().height > handle.viewport_bounds().size.height
};
(
bounce.into_any_element(),
scrollbar.into_any_element(),
Rc::new(can_scroll),
)
}
impl RenderOnce for ScrollArea {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let (frame_style, viewport_style) = split_style(self.style);
let (bounce, scrollbar, can_scroll) = match self.content {
Content::Children(children) => {
let handle = window
.use_keyed_state(
ElementId::NamedChild(self.id.clone().into(), "scroll".into()),
cx,
|_, _| ScrollHandle::new(),
)
.read(cx)
.clone();
let viewport = div()
.id(self.id.clone())
.test_support()
.size_full()
.overflow_y_scroll()
.track_scroll(&handle)
.refine_style(&viewport_style)
.children(children);
chrome(&self.id, &handle, viewport)
}
Content::List {
state,
row_hint,
render_item,
} => {
let hinted_width = window.use_keyed_state(
ElementId::NamedChild(self.id.clone().into(), "hinted".into()),
cx,
|_, _| None::<Pixels>,
);
let width = state.viewport_bounds().size.width;
if *hinted_width.read(cx) != Some(width) {
state.clone().with_uniform_item_height(row_hint);
hinted_width.update(cx, |hinted, _| *hinted = Some(width));
}
let viewport = div()
.id(self.id.clone())
.test_support()
.size_full()
.refine_style(&viewport_style)
.child(
list(state.clone(), render_item)
.with_sizing_behavior(ListSizingBehavior::Infer)
.size_full(),
);
chrome(&self.id, &state, viewport)
}
};
div()
.id(ElementId::NamedChild(self.id.into(), "frame".into()))
.relative()
.refine_style(&frame_style)
.on_scroll_wheel(move |_, _, cx| {
if can_scroll() {
cx.stop_propagation();
}
})
.child(bounce)
.child(scrollbar)
}
}
fn split_style(style: StyleRefinement) -> (StyleRefinement, StyleRefinement) {
let mut frame = StyleRefinement::default();
let mut viewport = style;
frame.size = std::mem::take(&mut viewport.size);
frame.min_size = std::mem::take(&mut viewport.min_size);
frame.max_size = std::mem::take(&mut viewport.max_size);
frame.flex_grow = viewport.flex_grow.take();
frame.flex_shrink = viewport.flex_shrink.take();
frame.flex_basis = viewport.flex_basis.take();
frame.align_self = viewport.align_self.take();
frame.margin = std::mem::take(&mut viewport.margin);
frame.position = viewport.position.take();
frame.inset = std::mem::take(&mut viewport.inset);
(frame, viewport)
}