Add ScrollView (#56)

This commit is contained in:
Jason Lee 2024-07-23 09:38:23 +08:00 committed by GitHub
parent 5034152f21
commit faa16fb660
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 249 additions and 18 deletions

View file

@ -2,12 +2,12 @@ use std::cell::Cell;
use std::rc::Rc;
use gpui::{
canvas, deferred, div, px, InteractiveElement, ParentElement, Pixels, Render, ScrollHandle,
canvas, div, px, InteractiveElement, ParentElement, Pixels, Render, ScrollHandle,
StatefulInteractiveElement as _, Styled, View, ViewContext, VisualContext, WindowContext,
};
use ui::button::Button;
use ui::divider::Divider;
use ui::scroll::{Scrollbar, ScrollbarAxis, ScrollbarState};
use ui::scroll::{scroll_view, Scrollbar, ScrollbarAxis, ScrollbarState};
use ui::theme::ActiveTheme;
use ui::{h_flex, v_flex, Clickable};
@ -121,16 +121,7 @@ impl Render for ScrollableStory {
div()
.relative()
.w_full()
.h(px(400.))
.child(deferred(
Scrollbar::both(
view,
self.scroll_state.clone(),
self.scroll_handle.clone(),
self.scroll_size,
)
.axis(self.axis),
))
.h(px(350.))
.child(
div()
.id("scroll-story")
@ -159,8 +150,44 @@ impl Render for ScrollableStory {
.size_full()
}),
),
)
.child(
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(
Scrollbar::both(
view,
self.scroll_state.clone(),
self.scroll_handle.clone(),
self.scroll_size,
)
.axis(self.axis),
),
),
),
)
.child({
let view = cx.view().clone();
let items = self.items.clone();
let test_width = self.test_width;
div()
.relative()
.border_1()
.border_color(cx.theme().border)
.w_full()
.h(px(200.))
.child(scroll_view("scrollview-1", view).content(move |cx| {
v_flex().m_3().w(test_width).gap_1().children(
items
.iter()
.map(|s| div().bg(cx.theme().card).child(s.clone())),
)
}))
})
}
}

View file

@ -2,7 +2,7 @@ use crate::{
h_flex,
indicator::Indicator,
theme::{ActiveTheme, Colorize as _},
Clickable, Disableable, Icon, Selectable, Size, StyledExt,
Clickable, Disableable, Icon, Selectable, Size,
};
use gpui::{
div, prelude::FluentBuilder as _, px, AnyElement, ClickEvent, DefiniteLength, Div, ElementId,
@ -235,9 +235,7 @@ impl RenderOnce for Button {
.border_color(normal_style.border)
.bg(normal_style.bg)
})
.when(focused, |this| {
this.border_color(cx.theme().ring).debug_blue()
})
.when(focused, |this| this.border_color(cx.theme().ring))
.when_some(
self.on_click.filter(|_| !self.disabled),
|this, on_click| {

View file

@ -1,5 +1,7 @@
mod scroll_view;
mod scrollable;
mod scrollbar;
pub use scroll_view::*;
pub use scrollable::*;
pub use scrollbar::*;

View file

@ -0,0 +1,195 @@
use std::{cell::Cell, rc::Rc};
use gpui::{
canvas, div, relative, AnyElement, AnyView, Element, ElementId, GlobalElementId,
InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Position, ScrollHandle,
SharedString, Size, StatefulInteractiveElement, Style, Styled as _, WindowContext,
};
use super::{Scrollbar, ScrollbarAxis, ScrollbarState};
pub fn scroll_view(id: impl Into<ElementId>, view: impl Into<AnyView>) -> ScrollView {
ScrollView::new(id, view, ScrollbarAxis::Both)
}
/// A scroll view is a container that allows the user to scroll through a large amount of content.
pub struct ScrollView {
id: ElementId,
view: AnyView,
axix: ScrollbarAxis,
content: Option<Box<dyn Fn(&mut WindowContext) -> AnyElement + 'static>>,
}
impl ScrollView {
fn new(id: impl Into<ElementId>, view: impl Into<AnyView>, axis: ScrollbarAxis) -> Self {
let view: AnyView = view.into();
Self {
id: ElementId::Name(SharedString::from(format!(
"{}-{}",
view.entity_id(),
id.into()
))),
view,
axix: axis,
content: None,
}
}
/// Set only a vertical scrollbar.
pub fn vertical(mut self) -> Self {
self.set_axis(ScrollbarAxis::Vertical);
self
}
/// Set only a horizontal scrollbar.
/// In current implementation, this is not supported yet.
pub fn horizontal(mut self) -> Self {
self.set_axis(ScrollbarAxis::Horizontal);
self
}
/// Set the content render of the scroll view.
#[must_use]
pub fn content<F, E>(mut self, builder: F) -> Self
where
F: Fn(&mut WindowContext) -> E + 'static,
E: IntoElement,
{
self.content = Some(Box::new(move |cx| builder(cx).into_any_element()));
self
}
/// Set the axis of the scroll view.
pub fn set_axis(&mut self, axis: ScrollbarAxis) {
self.axix = axis;
}
fn with_element_state<R>(
&mut self,
id: &GlobalElementId,
cx: &mut WindowContext,
f: impl FnOnce(&mut Self, &mut ScrollViewState, &mut WindowContext) -> R,
) -> R {
cx.with_optional_element_state::<ScrollViewState, _>(Some(id), |element_state, cx| {
let mut element_state = element_state.unwrap().unwrap_or_default();
let result = f(self, &mut element_state, cx);
(result, Some(element_state))
})
}
}
pub struct ScrollViewState {
scroll_size: Rc<Cell<Size<Pixels>>>,
state: Rc<Cell<ScrollbarState>>,
handle: ScrollHandle,
}
impl Default for ScrollViewState {
fn default() -> Self {
Self {
handle: ScrollHandle::new(),
scroll_size: Rc::new(Cell::new(Size::default())),
state: Rc::new(Cell::new(ScrollbarState::default())),
}
}
}
impl IntoElement for ScrollView {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for ScrollView {
type RequestLayoutState = AnyElement;
type PrepaintState = ScrollViewState;
fn id(&self) -> Option<gpui::ElementId> {
Some(self.id.clone())
}
fn request_layout(
&mut self,
id: Option<&gpui::GlobalElementId>,
cx: &mut gpui::WindowContext,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.flex_grow = 1.0;
style.position = Position::Relative;
style.size.width = relative(1.0).into();
style.size.height = relative(1.0).into();
let axix = self.axix;
let view = self.view.clone();
let scroll_id = self.id.clone();
let content = self.content.as_ref().map(|c| c(cx));
self.with_element_state(id.unwrap(), cx, |_, element_state, cx| {
let handle = element_state.handle.clone();
let state = element_state.state.clone();
let scroll_size = element_state.scroll_size.clone();
let mut element = div()
.relative()
.size_full()
.child(
div()
.id(scroll_id)
.track_scroll(&handle)
.overflow_scroll()
.relative()
.size_full()
.child(div().children(content).child({
let scroll_size = element_state.scroll_size.clone();
canvas(move |b, _| scroll_size.set(b.size), |_, _, _| {})
.absolute()
.size_full()
})),
)
.child(
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(
Scrollbar::both(view, state, handle.clone(), scroll_size.get())
.axis(axix),
),
)
.into_any_element();
let element_id = element.request_layout(cx);
let layout_id = cx.request_layout(style, vec![element_id]);
(layout_id, element)
})
}
fn prepaint(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: gpui::Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
cx: &mut gpui::WindowContext,
) -> Self::PrepaintState {
element.prepaint(cx);
// do nothing
ScrollViewState::default()
}
fn paint(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: gpui::Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut gpui::WindowContext,
) {
element.paint(cx)
}
}

View file

@ -293,7 +293,7 @@ impl Element for Scrollbar {
cx: &mut gpui::WindowContext,
) {
let hitbox_bounds = hitbox.bounds;
let is_both = self.axis.is_both();
let has_both = self.axis.is_both();
cx.with_content_mask(
Some(ContentMask {
@ -317,7 +317,7 @@ impl Element for Scrollbar {
};
// The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible.
let margin_end = if !is_vertical && is_both {
let margin_end = if has_both && !is_vertical {
self.width
} else {
px(0.)

View file

@ -77,6 +77,15 @@ pub trait StyledExt: Styled + Sized {
elevated(self, cx, ElevationIndex::ModalSurface)
}
/// Render a border with a width of 1px, color red
fn debug_red(self) -> Self {
if cfg!(debug_assertions) {
self.border_1().border_color(crate::red_500())
} else {
self
}
}
/// Render a border with a width of 1px, color blue
fn debug_blue(self) -> Self {
if cfg!(debug_assertions) {