Support overflow scroll

Nathan Sobo created

Change summary

crates/gpui2/src/elements/div.rs     | 161 ++++++++++++++++++++++++++++-
crates/gpui2/src/layout_context.rs   |   5 
crates/gpui2/src/style.rs            |   8 +
crates/storybook/src/collab_panel.rs |  48 +++++---
crates/storybook/src/workspace.rs    |  89 +++++++--------
5 files changed, 236 insertions(+), 75 deletions(-)

Detailed changes

crates/gpui2/src/elements/div.rs 🔗

@@ -1,17 +1,17 @@
-use std::cell::Cell;
+use std::{cell::Cell, rc::Rc};
 
 use crate::{
     element::{AnyElement, Element, IntoElement, Layout, ParentElement},
     hsla,
     layout_context::LayoutContext,
     paint_context::PaintContext,
-    style::{CornerRadii, Style, StyleHelpers, Styleable},
+    style::{CornerRadii, Overflow, Style, StyleHelpers, Styleable},
     InteractionHandlers, Interactive,
 };
 use anyhow::Result;
 use gpui::{
-    geometry::vector::Vector2F,
-    platform::{MouseButton, MouseButtonEvent, MouseMovedEvent},
+    geometry::{rect::RectF, vector::Vector2F, Point},
+    platform::{MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent},
     scene::{self},
     LayoutId,
 };
@@ -23,6 +23,7 @@ pub struct Div<V: 'static> {
     styles: RefinementCascade<Style>,
     handlers: InteractionHandlers<V>,
     children: SmallVec<[AnyElement<V>; 2]>,
+    scroll_state: Option<ScrollState>,
 }
 
 pub fn div<V>() -> Div<V> {
@@ -30,11 +31,12 @@ pub fn div<V>() -> Div<V> {
         styles: Default::default(),
         handlers: Default::default(),
         children: Default::default(),
+        scroll_state: None,
     }
 }
 
 impl<V: 'static> Element<V> for Div<V> {
-    type PaintState = ();
+    type PaintState = Vec<LayoutId>;
 
     fn layout(
         &mut self,
@@ -59,7 +61,7 @@ impl<V: 'static> Element<V> for Div<V> {
             cx.pop_text_style();
         }
 
-        Ok((cx.add_layout_node(style, children)?, ()))
+        Ok((cx.add_layout_node(style, children.clone())?, children))
     }
 
     fn paint(
@@ -67,27 +69,45 @@ impl<V: 'static> Element<V> for Div<V> {
         view: &mut V,
         parent_origin: Vector2F,
         layout: &Layout,
-        _: &mut Self::PaintState,
+        child_layouts: &mut Vec<LayoutId>,
         cx: &mut PaintContext<V>,
     ) where
         Self: Sized,
     {
         let order = layout.order;
         let bounds = layout.bounds + parent_origin;
+
         let style = self.computed_style();
         let pop_text_style = style.text_style(cx).map_or(false, |style| {
             cx.push_text_style(&style).log_err().is_some()
         });
         style.paint_background(bounds, cx);
         self.interaction_handlers().paint(order, bounds, cx);
+
+        let scrolled_origin = bounds.origin() - self.scroll_offset(&style.overflow);
+
+        // TODO: Support only one dimension being hidden
+        let mut pop_layer = false;
+        if style.overflow.y != Overflow::Visible || style.overflow.x != Overflow::Visible {
+            cx.scene.push_layer(Some(bounds));
+            pop_layer = true;
+        }
+
         for child in &mut self.children {
-            child.paint(view, bounds.origin(), cx);
+            child.paint(view, scrolled_origin, cx);
+        }
+
+        if pop_layer {
+            cx.scene.pop_layer();
         }
+
         style.paint_foreground(bounds, cx);
         if pop_text_style {
             cx.pop_text_style();
         }
 
+        self.handle_scroll(order, bounds, style.overflow.clone(), child_layouts, cx);
+
         if cx.is_inspector_enabled() {
             self.paint_inspector(parent_origin, layout, cx);
         }
@@ -95,6 +115,106 @@ impl<V: 'static> Element<V> for Div<V> {
 }
 
 impl<V: 'static> Div<V> {
+    pub fn overflow_hidden(mut self) -> Self {
+        self.declared_style().overflow.x = Some(Overflow::Hidden);
+        self.declared_style().overflow.y = Some(Overflow::Hidden);
+        self
+    }
+
+    pub fn overflow_hidden_x(mut self) -> Self {
+        self.declared_style().overflow.x = Some(Overflow::Hidden);
+        self
+    }
+
+    pub fn overflow_hidden_y(mut self) -> Self {
+        self.declared_style().overflow.y = Some(Overflow::Hidden);
+        self
+    }
+
+    pub fn overflow_scroll(mut self, scroll_state: ScrollState) -> Self {
+        self.scroll_state = Some(scroll_state);
+        self.declared_style().overflow.x = Some(Overflow::Scroll);
+        self.declared_style().overflow.y = Some(Overflow::Scroll);
+        self
+    }
+
+    pub fn overflow_x_scroll(mut self, scroll_state: ScrollState) -> Self {
+        self.scroll_state = Some(scroll_state);
+        self.declared_style().overflow.x = Some(Overflow::Scroll);
+        self
+    }
+
+    pub fn overflow_y_scroll(mut self, scroll_state: ScrollState) -> Self {
+        self.scroll_state = Some(scroll_state);
+        self.declared_style().overflow.y = Some(Overflow::Scroll);
+        self
+    }
+
+    fn scroll_offset(&self, overflow: &Point<Overflow>) -> Vector2F {
+        let mut offset = Vector2F::zero();
+        if overflow.y == Overflow::Scroll {
+            offset.set_y(self.scroll_state.as_ref().unwrap().y());
+        }
+        if overflow.x == Overflow::Scroll {
+            offset.set_x(self.scroll_state.as_ref().unwrap().x());
+        }
+
+        offset
+    }
+
+    fn handle_scroll(
+        &mut self,
+        order: u32,
+        bounds: RectF,
+        overflow: Point<Overflow>,
+        child_layout_ids: &[LayoutId],
+        cx: &mut PaintContext<V>,
+    ) {
+        if overflow.y == Overflow::Scroll || overflow.x == Overflow::Scroll {
+            let mut scroll_max = Vector2F::zero();
+            for child_layout_id in child_layout_ids {
+                if let Some(child_layout) = cx
+                    .layout_engine()
+                    .unwrap()
+                    .computed_layout(*child_layout_id)
+                    .log_err()
+                {
+                    scroll_max = scroll_max.max(child_layout.bounds.lower_right());
+                }
+            }
+            scroll_max -= bounds.size();
+
+            let scroll_state = self.scroll_state.as_ref().unwrap().clone();
+            cx.on_event(order, move |_, event: &ScrollWheelEvent, cx| {
+                if bounds.contains_point(event.position) {
+                    let scroll_delta = match event.delta {
+                        gpui::platform::ScrollDelta::Pixels(delta) => delta,
+                        gpui::platform::ScrollDelta::Lines(delta) => {
+                            delta * cx.text_style().font_size
+                        }
+                    };
+                    if overflow.x == Overflow::Scroll {
+                        scroll_state.set_x(
+                            (scroll_state.x() - scroll_delta.x())
+                                .max(0.)
+                                .min(scroll_max.x()),
+                        );
+                    }
+                    if overflow.y == Overflow::Scroll {
+                        scroll_state.set_y(
+                            (scroll_state.y() - scroll_delta.y())
+                                .max(0.)
+                                .min(scroll_max.y()),
+                        );
+                    }
+                    cx.repaint();
+                } else {
+                    cx.bubble_event();
+                }
+            })
+        }
+    }
+
     fn paint_inspector(&self, parent_origin: Vector2F, layout: &Layout, cx: &mut PaintContext<V>) {
         let style = self.styles.merged();
         let bounds = layout.bounds + parent_origin;
@@ -175,3 +295,28 @@ impl<V: 'static> IntoElement<V> for Div<V> {
         self
     }
 }
+
+#[derive(Default, Clone)]
+pub struct ScrollState(Rc<Cell<Vector2F>>);
+
+impl ScrollState {
+    pub fn x(&self) -> f32 {
+        self.0.get().x()
+    }
+
+    pub fn set_x(&self, value: f32) {
+        let mut current_value = self.0.get();
+        current_value.set_x(value);
+        self.0.set(current_value);
+    }
+
+    pub fn y(&self) -> f32 {
+        self.0.get().y()
+    }
+
+    pub fn set_y(&self, value: f32) {
+        let mut current_value = self.0.get();
+        current_value.set_y(value);
+        self.0.set(current_value);
+    }
+}

crates/gpui2/src/layout_context.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{element::LayoutId, style::Style};
 use anyhow::{anyhow, Result};
 use derive_more::{Deref, DerefMut};
-use gpui::{geometry::Size, MeasureParams};
+use gpui::{geometry::Size, taffy::style::Overflow, MeasureParams};
 pub use gpui::{taffy::tree::NodeId, LayoutContext as LegacyLayoutContext};
 
 #[derive(Deref, DerefMut)]
@@ -22,11 +22,12 @@ impl<'a, 'b, 'c, 'd, V: 'static> LayoutContext<'a, 'b, 'c, 'd, V> {
         children: impl IntoIterator<Item = NodeId>,
     ) -> Result<LayoutId> {
         let rem_size = self.rem_size();
+        let style = style.to_taffy(rem_size);
         let id = self
             .legacy_cx
             .layout_engine()
             .ok_or_else(|| anyhow!("no layout engine"))?
-            .add_node(style.to_taffy(rem_size), children)?;
+            .add_node(style, children)?;
 
         Ok(id)
     }

crates/gpui2/src/style.rs 🔗

@@ -439,6 +439,14 @@ pub trait StyleHelpers: Styleable<Style = Style> {
         self
     }
 
+    fn grow(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().flex_grow = Some(1.);
+        self
+    }
+
     fn items_start(mut self) -> Self
     where
         Self: Sized,

crates/storybook/src/collab_panel.rs 🔗

@@ -1,6 +1,6 @@
 use crate::theme::{theme, Theme};
 use gpui2::{
-    elements::{div, img, svg},
+    elements::{div, div::ScrollState, img, svg},
     style::{StyleHelpers, Styleable},
     ArcCow, Element, IntoElement, ParentElement, ViewContext,
 };
@@ -9,11 +9,15 @@ use std::marker::PhantomData;
 #[derive(Element)]
 pub struct CollabPanelElement<V: 'static> {
     view_type: PhantomData<V>,
+    scroll_state: ScrollState,
 }
 
-pub fn collab_panel<V: 'static>() -> CollabPanelElement<V> {
+// When I improve child view rendering, I'd like to have V implement a trait  that
+// provides the scroll state, among other things.
+pub fn collab_panel<V: 'static>(scroll_state: ScrollState) -> CollabPanelElement<V> {
     CollabPanelElement {
         view_type: PhantomData,
+        scroll_state,
     }
 }
 
@@ -24,6 +28,7 @@ impl<V: 'static> CollabPanelElement<V> {
         // Panel
         div()
             .w_64()
+            .h_full()
             .flex()
             .flex_col()
             .font("Zed Sans Extended")
@@ -36,6 +41,7 @@ impl<V: 'static> CollabPanelElement<V> {
                     .w_full()
                     .flex()
                     .flex_col()
+                    .overflow_y_scroll(self.scroll_state.clone())
                     // List Container
                     .child(
                         div()
@@ -67,21 +73,29 @@ impl<V: 'static> CollabPanelElement<V> {
                             .flex()
                             .flex_col()
                             .child(self.list_section_header("CONTACTS", true, theme))
-                            .child(self.list_item(
-                                "http://github.com/as-cii.png?s=50",
-                                "as-cii",
-                                theme,
-                            ))
-                            .child(self.list_item(
-                                "http://github.com/nathansobo.png?s=50",
-                                "nathansobo",
-                                theme,
-                            ))
-                            .child(self.list_item(
-                                "http://github.com/maxbrunsfeld.png?s=50",
-                                "maxbrunsfeld",
-                                theme,
-                            )),
+                            .children(
+                                std::iter::repeat_with(|| {
+                                    vec![
+                                        self.list_item(
+                                            "http://github.com/as-cii.png?s=50",
+                                            "as-cii",
+                                            theme,
+                                        ),
+                                        self.list_item(
+                                            "http://github.com/nathansobo.png?s=50",
+                                            "nathansobo",
+                                            theme,
+                                        ),
+                                        self.list_item(
+                                            "http://github.com/maxbrunsfeld.png?s=50",
+                                            "maxbrunsfeld",
+                                            theme,
+                                        ),
+                                    ]
+                                })
+                                .take(10)
+                                .flatten(),
+                            ),
                     ),
             )
             .child(

crates/storybook/src/workspace.rs 🔗

@@ -1,10 +1,50 @@
 use crate::{collab_panel::collab_panel, theme::theme};
 use gpui2::{
-    elements::{div, img, svg},
+    elements::{div, div::ScrollState, img, svg},
     style::{StyleHelpers, Styleable},
     Element, IntoElement, ParentElement, ViewContext,
 };
 
+#[derive(Element, Default)]
+struct WorkspaceElement {
+    left_scroll_state: ScrollState,
+    right_scroll_state: ScrollState,
+}
+
+pub fn workspace<V: 'static>() -> impl Element<V> {
+    WorkspaceElement::default()
+}
+
+impl WorkspaceElement {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .size_full()
+            .flex()
+            .flex_col()
+            .font("Zed Sans Extended")
+            .gap_0()
+            .justify_start()
+            .items_start()
+            .text_color(theme.lowest.base.default.foreground)
+            .fill(theme.middle.base.default.background)
+            .child(titlebar())
+            .child(
+                div()
+                    .flex_1()
+                    .w_full()
+                    .flex()
+                    .flex_row()
+                    .overflow_hidden()
+                    .child(collab_panel(self.left_scroll_state.clone()))
+                    .child(div().h_full().flex_1())
+                    .child(collab_panel(self.right_scroll_state.clone())),
+            )
+            .child(statusbar())
+    }
+}
+
 #[derive(Element)]
 struct TitleBar;
 
@@ -393,50 +433,3 @@ impl StatusBar {
             )
     }
 }
-
-// ================================================================================ //
-
-#[derive(Element)]
-struct WorkspaceElement;
-
-pub fn workspace<V: 'static>() -> impl Element<V> {
-    WorkspaceElement
-}
-
-impl WorkspaceElement {
-    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-
-        div()
-            .size_full()
-            .flex()
-            .flex_col()
-            .font("Zed Sans Extended")
-            .gap_0()
-            .justify_start()
-            .items_start()
-            .text_color(theme.lowest.base.default.foreground)
-            // .fill(theme.middle.warning.default.background)
-            .child(titlebar())
-            .child(
-                div()
-                    .flex_1()
-                    .flex()
-                    .flex_row()
-                    .w_full()
-                    .child(collab_panel())
-                    .child(div().h_full().flex_1())
-                    .child(collab_panel()),
-            )
-            .child(statusbar())
-    }
-}
-
-// Hover over things
-// Paint its space... padding, margin, border, content
-
-/*
-* h_8, grow_0/flex_grow_0, shrink_0/flex_shrink_0
-* flex_grow
-* h_8, grow_0/flex_grow_0, shrink_0/flex_shrink_0
-*/