diff --git a/crates/story/src/scrollable_story.rs b/crates/story/src/scrollable_story.rs index f17104ae..e657b9cb 100644 --- a/crates/story/src/scrollable_story.rs +++ b/crates/story/src/scrollable_story.rs @@ -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())), + ) + })) + }) } } diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index c9f35f40..b60a981e 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -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| { diff --git a/crates/ui/src/scroll/mod.rs b/crates/ui/src/scroll/mod.rs index 453b191d..e3190b4d 100644 --- a/crates/ui/src/scroll/mod.rs +++ b/crates/ui/src/scroll/mod.rs @@ -1,5 +1,7 @@ +mod scroll_view; mod scrollable; mod scrollbar; +pub use scroll_view::*; pub use scrollable::*; pub use scrollbar::*; diff --git a/crates/ui/src/scroll/scroll_view.rs b/crates/ui/src/scroll/scroll_view.rs new file mode 100644 index 00000000..fba771b0 --- /dev/null +++ b/crates/ui/src/scroll/scroll_view.rs @@ -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, view: impl Into) -> 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 AnyElement + 'static>>, +} + +impl ScrollView { + fn new(id: impl Into, view: impl Into, 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(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( + &mut self, + id: &GlobalElementId, + cx: &mut WindowContext, + f: impl FnOnce(&mut Self, &mut ScrollViewState, &mut WindowContext) -> R, + ) -> R { + cx.with_optional_element_state::(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>>, + state: Rc>, + 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 { + 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, + 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, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + cx: &mut gpui::WindowContext, + ) { + element.paint(cx) + } +} diff --git a/crates/ui/src/scroll/scrollbar.rs b/crates/ui/src/scroll/scrollbar.rs index e6854194..59db0b89 100644 --- a/crates/ui/src/scroll/scrollbar.rs +++ b/crates/ui/src/scroll/scrollbar.rs @@ -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.) diff --git a/crates/ui/src/styled_ext.rs b/crates/ui/src/styled_ext.rs index 503ea60c..c83f8a21 100644 --- a/crates/ui/src/styled_ext.rs +++ b/crates/ui/src/styled_ext.rs @@ -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) {