diff --git a/crates/story/src/scrollable_story.rs b/crates/story/src/scrollable_story.rs index 5061b152..6e0216bc 100644 --- a/crates/story/src/scrollable_story.rs +++ b/crates/story/src/scrollable_story.rs @@ -7,9 +7,9 @@ use gpui::{ }; use ui::button::Button; use ui::divider::Divider; -use ui::scroll::{Scrollable, Scrollbar, ScrollbarAxis, ScrollbarState}; +use ui::scroll::{Scrollbar, ScrollbarAxis, ScrollbarState}; use ui::theme::ActiveTheme; -use ui::{h_flex, v_flex, Clickable}; +use ui::{h_flex, v_flex, Clickable, StyledExt as _}; pub struct ScrollableStory { scroll_handle: ScrollHandle, @@ -182,15 +182,18 @@ impl Render for ScrollableStory { .h(px(200.)) .child( v_flex() - .m_3() + .id("test-1") + .focusable() + .scrollable(cx.view().clone(), ScrollbarAxis::Vertical) + .p_3() .w(test_width) .gap_1() + .child("Hello world") .children( items .iter() .map(|s| div().bg(cx.theme().card).child(s.clone())), - ) - .scrollable("scroll-view1", cx.view().clone()), + ), ) }) } diff --git a/crates/ui/src/scroll/mod.rs b/crates/ui/src/scroll/mod.rs index 782dd96e..ac47fb5b 100644 --- a/crates/ui/src/scroll/mod.rs +++ b/crates/ui/src/scroll/mod.rs @@ -1,37 +1,7 @@ -mod scroll_view; mod scrollable; +mod scrollable_mask; mod scrollbar; -use gpui::{AnyElement, AnyView, Div, Element, ElementId, Focusable, InteractiveElement, Stateful}; -pub use scroll_view::*; pub use scrollable::*; +pub use scrollable_mask::*; pub use scrollbar::*; - -pub trait Scrollable: Element { - /// Wraps the element in a ScrollView. - /// - /// Current this is only have a vertical scrollbar. - fn scrollable(self, id: impl Into, view: impl Into) -> ScrollView { - ScrollView::new( - ElementId::Name(format!("{}:{}", id.into(), "ScrollView").into()), - view, - ScrollbarAxis::Vertical, - ) - .content(move |_| self) - } -} - -impl Scrollable for AnyElement {} -impl Scrollable for Div {} -impl Scrollable for Focusable -where - E: Element, - Self: InteractiveElement, -{ -} -impl Scrollable for Stateful -where - E: Element, - Self: InteractiveElement, -{ -} diff --git a/crates/ui/src/scroll/scroll_view.rs b/crates/ui/src/scroll/scroll_view.rs deleted file mode 100644 index 6f0a2d3a..00000000 --- a/crates/ui/src/scroll/scroll_view.rs +++ /dev/null @@ -1,199 +0,0 @@ -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 { - pub(super) 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: FnOnce(&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.take().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/scrollable.rs b/crates/ui/src/scroll/scrollable.rs index 5604603d..4cd148d0 100644 --- a/crates/ui/src/scroll/scrollable.rs +++ b/crates/ui/src/scroll/scrollable.rs @@ -1,55 +1,119 @@ +use std::{cell::Cell, rc::Rc}; + +use super::{Scrollbar, ScrollbarAxis, ScrollbarState}; use gpui::{ - px, relative, AnyView, Bounds, ContentMask, Corners, Edges, Element, ElementId, - GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point, - Position, ScrollHandle, ScrollWheelEvent, Style, WindowContext, + canvas, div, relative, AnyElement, AnyView, Element, ElementId, GlobalElementId, + InteractiveElement as _, IntoElement, ParentElement, Pixels, Position, ScrollHandle, + SharedString, Size, StatefulInteractiveElement, Style, StyleRefinement, Styled, WindowContext, }; -/// The scroll axis direction. -#[allow(dead_code)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ScrollableAxis { - /// Horizontal scroll. - Horizontal, - /// Vertical scroll. - Vertical, -} - -/// Make a scrollable mask element to cover the parent view with the mouse wheel event listening. -/// -/// When the mouse wheel is scrolled, will move the `scroll_handle` scrolling with the `axis` direction. -/// You can use this `scroll_handle` to control what you want to scroll. -/// This is only can handle once axis scrolling. -pub struct ScrollableMask { +/// A scroll view is a container that allows the user to scroll through a large amount of content. +pub struct Scrollable { + id: ElementId, + element: Option, view: AnyView, - axis: ScrollableAxis, - scroll_handle: ScrollHandle, - debug: Option, + axis: ScrollbarAxis, + /// This is not used yet. + _style: StyleRefinement, } -impl ScrollableMask { - /// Create a new scrollable mask element. - pub fn new( - view: impl Into, - axis: ScrollableAxis, - scroll_handle: &ScrollHandle, - ) -> Self { +impl Scrollable +where + E: Element, +{ + pub(crate) fn new(element: E, view: impl Into, axis: ScrollbarAxis) -> Self { + let view: AnyView = view.into(); + let id = ElementId::Name(SharedString::from(format!( + "ScrollView:{}-{:?}", + view.entity_id(), + element.id(), + ))); + Self { - view: view.into(), - scroll_handle: scroll_handle.clone(), + element: Some(element), + id, + view, axis, - debug: None, + _style: StyleRefinement::default(), } } - /// Enable the debug border, to show the mask bounds. - #[allow(dead_code)] - pub fn debug(mut self) -> Self { - self.debug = Some(gpui::yellow()); + /// 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 axis of the scroll view. + pub fn set_axis(&mut self, axis: ScrollbarAxis) { + self.axis = 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)) + }) + } } -impl IntoElement for ScrollableMask { +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 ParentElement for Scrollable +where + E: Element + ParentElement, +{ + fn extend(&mut self, elements: impl IntoIterator) { + if let Some(element) = &mut self.element { + element.extend(elements); + } + } +} + +impl Styled for Scrollable +where + E: Element + Styled, +{ + fn style(&mut self) -> &mut StyleRefinement { + if let Some(element) = &mut self.element { + element.style() + } else { + &mut self._style + } + } +} + +impl IntoElement for Scrollable +where + E: Element, +{ type Element = Self; fn into_element(self) -> Self::Element { @@ -57,103 +121,97 @@ impl IntoElement for ScrollableMask { } } -impl Element for ScrollableMask { - type RequestLayoutState = (); - type PrepaintState = Hitbox; +impl Element for Scrollable +where + E: Element, +{ + type RequestLayoutState = AnyElement; + type PrepaintState = ScrollViewState; - fn id(&self) -> Option { - None + fn id(&self) -> Option { + Some(self.id.clone()) } fn request_layout( &mut self, - _: Option<&GlobalElementId>, - cx: &mut WindowContext, - ) -> (LayoutId, Self::RequestLayoutState) { + id: Option<&gpui::GlobalElementId>, + cx: &mut gpui::WindowContext, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { let mut style = Style::default(); - // Set the layout style relative to the table view to get same size. - style.position = Position::Absolute; style.flex_grow = 1.0; - style.flex_shrink = 1.0; - style.size.width = relative(1.).into(); - style.size.height = relative(1.).into(); + style.position = Position::Relative; + style.size.width = relative(1.0).into(); + style.size.height = relative(1.0).into(); - (cx.request_layout(style, None), ()) + let axis = self.axis; + let view = self.view.clone(); + + let scroll_id = self.id.clone(); + let content = self.element.take().map(|c| c.into_any_element()); + + 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(axis), + ), + ) + .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<&GlobalElementId>, - bounds: Bounds, - _: &mut Self::RequestLayoutState, - cx: &mut WindowContext, + _: Option<&gpui::GlobalElementId>, + _: gpui::Bounds, + element: &mut Self::RequestLayoutState, + cx: &mut gpui::WindowContext, ) -> Self::PrepaintState { - // Move y to bounds height to cover the parent view. - let cover_bounds = Bounds { - origin: Point { - x: bounds.origin.x, - y: bounds.origin.y - bounds.size.height, - }, - size: bounds.size, - }; - - cx.insert_hitbox(cover_bounds, false) + element.prepaint(cx); + // do nothing + ScrollViewState::default() } fn paint( &mut self, - _: Option<&GlobalElementId>, - _: Bounds, - _: &mut Self::RequestLayoutState, - hitbox: &mut Self::PrepaintState, - cx: &mut WindowContext, + _: Option<&gpui::GlobalElementId>, + _: gpui::Bounds, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + cx: &mut gpui::WindowContext, ) { - let line_height = cx.line_height(); - let bounds = hitbox.bounds; - - cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - if let Some(color) = self.debug { - cx.paint_quad(PaintQuad { - bounds, - border_widths: Edges::all(px(1.0)), - border_color: color, - background: gpui::transparent_white(), - corner_radii: Corners::all(px(0.)), - }); - } - - cx.on_mouse_event({ - let mouse_position = cx.mouse_position(); - let scroll_handle = self.scroll_handle.clone(); - let old_offset = scroll_handle.offset(); - let view_id = self.view.entity_id(); - let is_horizontal = self.axis == ScrollableAxis::Horizontal; - - move |event: &ScrollWheelEvent, _, cx| { - if bounds.contains(&mouse_position) { - let delta = event.delta.pixel_delta(line_height); - - if is_horizontal && !delta.x.is_zero() { - // When is horizontal scroll, move the horizontal scroll handle to make scrolling. - let mut offset = scroll_handle.offset(); - offset.x += delta.x; - scroll_handle.set_offset(offset); - } - - if !is_horizontal && !delta.y.is_zero() { - // When is vertical scroll, move the vertical scroll handle to make scrolling. - let mut offset = scroll_handle.offset(); - offset.y += delta.y; - scroll_handle.set_offset(offset); - } - - if old_offset != scroll_handle.offset() { - cx.notify(view_id); - cx.stop_propagation(); - } - } - } - }); - }); + element.paint(cx) } } diff --git a/crates/ui/src/scroll/scrollable_mask.rs b/crates/ui/src/scroll/scrollable_mask.rs new file mode 100644 index 00000000..5604603d --- /dev/null +++ b/crates/ui/src/scroll/scrollable_mask.rs @@ -0,0 +1,159 @@ +use gpui::{ + px, relative, AnyView, Bounds, ContentMask, Corners, Edges, Element, ElementId, + GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point, + Position, ScrollHandle, ScrollWheelEvent, Style, WindowContext, +}; + +/// The scroll axis direction. +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ScrollableAxis { + /// Horizontal scroll. + Horizontal, + /// Vertical scroll. + Vertical, +} + +/// Make a scrollable mask element to cover the parent view with the mouse wheel event listening. +/// +/// When the mouse wheel is scrolled, will move the `scroll_handle` scrolling with the `axis` direction. +/// You can use this `scroll_handle` to control what you want to scroll. +/// This is only can handle once axis scrolling. +pub struct ScrollableMask { + view: AnyView, + axis: ScrollableAxis, + scroll_handle: ScrollHandle, + debug: Option, +} + +impl ScrollableMask { + /// Create a new scrollable mask element. + pub fn new( + view: impl Into, + axis: ScrollableAxis, + scroll_handle: &ScrollHandle, + ) -> Self { + Self { + view: view.into(), + scroll_handle: scroll_handle.clone(), + axis, + debug: None, + } + } + + /// Enable the debug border, to show the mask bounds. + #[allow(dead_code)] + pub fn debug(mut self) -> Self { + self.debug = Some(gpui::yellow()); + self + } +} + +impl IntoElement for ScrollableMask { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for ScrollableMask { + type RequestLayoutState = (); + type PrepaintState = Hitbox; + + fn id(&self) -> Option { + None + } + + fn request_layout( + &mut self, + _: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + // Set the layout style relative to the table view to get same size. + style.position = Position::Absolute; + style.flex_grow = 1.0; + style.flex_shrink = 1.0; + style.size.width = relative(1.).into(); + style.size.height = relative(1.).into(); + + (cx.request_layout(style, None), ()) + } + + fn prepaint( + &mut self, + _: Option<&GlobalElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> Self::PrepaintState { + // Move y to bounds height to cover the parent view. + let cover_bounds = Bounds { + origin: Point { + x: bounds.origin.x, + y: bounds.origin.y - bounds.size.height, + }, + size: bounds.size, + }; + + cx.insert_hitbox(cover_bounds, false) + } + + fn paint( + &mut self, + _: Option<&GlobalElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + hitbox: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + let line_height = cx.line_height(); + let bounds = hitbox.bounds; + + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + if let Some(color) = self.debug { + cx.paint_quad(PaintQuad { + bounds, + border_widths: Edges::all(px(1.0)), + border_color: color, + background: gpui::transparent_white(), + corner_radii: Corners::all(px(0.)), + }); + } + + cx.on_mouse_event({ + let mouse_position = cx.mouse_position(); + let scroll_handle = self.scroll_handle.clone(); + let old_offset = scroll_handle.offset(); + let view_id = self.view.entity_id(); + let is_horizontal = self.axis == ScrollableAxis::Horizontal; + + move |event: &ScrollWheelEvent, _, cx| { + if bounds.contains(&mouse_position) { + let delta = event.delta.pixel_delta(line_height); + + if is_horizontal && !delta.x.is_zero() { + // When is horizontal scroll, move the horizontal scroll handle to make scrolling. + let mut offset = scroll_handle.offset(); + offset.x += delta.x; + scroll_handle.set_offset(offset); + } + + if !is_horizontal && !delta.y.is_zero() { + // When is vertical scroll, move the vertical scroll handle to make scrolling. + let mut offset = scroll_handle.offset(); + offset.y += delta.y; + scroll_handle.set_offset(offset); + } + + if old_offset != scroll_handle.offset() { + cx.notify(view_id); + cx.stop_propagation(); + } + } + } + }); + }); + } +} diff --git a/crates/ui/src/styled_ext.rs b/crates/ui/src/styled_ext.rs index 015fc3a2..2633db90 100644 --- a/crates/ui/src/styled_ext.rs +++ b/crates/ui/src/styled_ext.rs @@ -1,5 +1,11 @@ -use crate::theme::ActiveTheme; -use gpui::{hsla, point, px, rems, Axis, BoxShadow, FocusHandle, Pixels, Styled, WindowContext}; +use crate::{ + scroll::{Scrollable, ScrollbarAxis}, + theme::ActiveTheme, +}; +use gpui::{ + hsla, point, px, rems, AnyView, Axis, BoxShadow, Element, FocusHandle, Pixels, Styled, + WindowContext, +}; use smallvec::{smallvec, SmallVec}; pub enum ElevationIndex { @@ -149,6 +155,16 @@ pub trait StyledExt: Styled + Sized { self.border_color(cx.theme().ring) } + /// Wraps the element in a ScrollView. + /// + /// Current this is only have a vertical scrollbar. + fn scrollable(self, view: impl Into, axis: ScrollbarAxis) -> Scrollable + where + Self: Element, + { + Scrollable::new(self, view, axis) + } + font_weight!(font_thin, THIN); font_weight!(font_extralight, EXTRA_LIGHT); font_weight!(font_light, LIGHT);