use std::{cell::Cell, rc::Rc};
use gpui_kit::{
AnyElement, App, Bounds, ElementId, InteractiveElement as _, IntoElement, ListSizingBehavior,
ListState, ParentElement, Pixels, RenderOnce, ScrollHandle, StatefulInteractiveElement as _,
StyleRefinement, Styled, Window,
base::{
self, ElementExt as _, ScrollBounce, ScrollbarHandle, StyledExt as _, TestSupportExt as _,
},
div, list, point,
prelude::FluentBuilder as _,
px,
};
type RenderItem = Box<dyn FnMut(usize, &mut Window, &mut App) -> AnyElement>;
type OnScroll = Box<dyn Fn(&mut Window, &mut App)>;
enum Content {
Children(Vec<AnyElement>),
List {
state: ListState,
row_hint: Pixels,
render_item: RenderItem,
},
}
#[derive(IntoElement)]
pub struct ScrollArea {
id: ElementId,
content: Content,
handle: Option<ScrollHandle>,
on_scroll: Option<OnScroll>,
style: StyleRefinement,
}
impl ScrollArea {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
content: Content::Children(Vec::new()),
handle: None,
on_scroll: None,
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),
},
handle: None,
on_scroll: None,
style: StyleRefinement::default(),
}
}
}
impl ScrollArea {
pub fn track(mut self, handle: &ScrollHandle) -> Self {
self.handle = Some(handle.clone());
self
}
pub fn on_scroll(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_scroll = Some(Box::new(handler));
self
}
}
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,
on_scroll: Option<OnScroll>,
) -> (AnyElement, AnyElement) {
let mut bounce = ScrollBounce::new(
ElementId::NamedChild(id.clone().into(), "bounce".into()),
handle,
viewport,
)
.enabled(true);
if let Some(handler) = on_scroll {
bounce = bounce.on_scroll(handler);
}
let scrollbar = base::Scrollbar::vertical(handle)
.id(ElementId::NamedChild(id.clone().into(), "scrollbar".into()));
(bounce.into_any_element(), scrollbar.into_any_element())
}
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 on_scroll = self.on_scroll;
let mut revealer = None;
let (bounce, scrollbar) = match self.content {
Content::Children(children) => {
let handle = match self.handle {
Some(handle) => handle,
None => 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);
revealer = Some(handle.clone());
chrome(&self.id, &handle, viewport, on_scroll)
}
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, on_scroll)
}
};
let outside = Rc::new(Cell::new(None));
div()
.id(ElementId::NamedChild(self.id.into(), "frame".into()))
.relative()
.refine_style(&frame_style)
.when(revealer.is_some(), |this| {
let outside = outside.clone();
this.on_prepaint(move |_, window, _| outside.set(window.take_autoscroll()))
})
.child(bounce)
.child(scrollbar)
.when_some(revealer, |this, handle| {
this.on_prepaint(move |visible, window, _| match window.take_autoscroll() {
Some(request) => reveal(&handle, request, visible, window),
None => {
if let Some(request) = outside.take() {
window.request_autoscroll(request);
}
}
})
})
}
}
fn reveal(
handle: &ScrollHandle,
request: Bounds<Pixels>,
visible: Bounds<Pixels>,
window: &mut Window,
) {
let offset = handle.offset();
let shift = if request.top() < visible.top() || request.size.height > visible.size.height {
visible.top() - request.top()
} else if request.bottom() > visible.bottom() {
visible.bottom() - request.bottom()
} else {
px(0.)
};
let y = (offset.y + shift).clamp(-handle.max_offset().y, px(0.));
let moved = y - offset.y;
if moved != px(0.) {
handle.set_offset(point(offset.x, y));
window.request_animation_frame();
}
let shown =
Bounds::new(request.origin + point(px(0.), moved), request.size).intersect(&visible);
if shown.size.height > px(0.) {
window.request_autoscroll(shown);
}
}
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)
}