Make the tabs scrollable when they overflow

Nathan Sobo created

This adds the ability to make a Flex element scrollable by passing a type tag and instance id, which we use to store the scroll position in an ElementStateHandle.

Still need to allow the element to auto-scroll.

Change summary

crates/gpui/src/elements/flex.rs | 75 ++++++++++++++++++++++++++++++++-
crates/workspace/src/pane.rs     |  2 
2 files changed, 72 insertions(+), 5 deletions(-)

Detailed changes

crates/gpui/src/elements/flex.rs 🔗

@@ -2,8 +2,8 @@ use std::{any::Any, f32::INFINITY};
 
 use crate::{
     json::{self, ToJson, Value},
-    Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
-    SizeConstraint, Vector2FExt,
+    Axis, DebugContext, Element, ElementBox, ElementStateContext, ElementStateHandle, Event,
+    EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
 };
 use pathfinder_geometry::{
     rect::RectF,
@@ -11,9 +11,15 @@ use pathfinder_geometry::{
 };
 use serde_json::json;
 
+#[derive(Default)]
+struct ScrollState {
+    scroll_position: f32,
+}
+
 pub struct Flex {
     axis: Axis,
     children: Vec<ElementBox>,
+    scroll_state: Option<ElementStateHandle<ScrollState>>,
 }
 
 impl Flex {
@@ -21,6 +27,7 @@ impl Flex {
         Self {
             axis,
             children: Default::default(),
+            scroll_state: None,
         }
     }
 
@@ -32,6 +39,15 @@ impl Flex {
         Self::new(Axis::Vertical)
     }
 
+    pub fn scrollable<Tag, C>(mut self, element_id: usize, cx: &mut C) -> Self
+    where
+        Tag: 'static,
+        C: ElementStateContext,
+    {
+        self.scroll_state = Some(cx.element_state::<Tag, ScrollState>(element_id));
+        self
+    }
+
     fn layout_flex_children(
         &mut self,
         layout_expanded: bool,
@@ -167,6 +183,13 @@ impl Element for Flex {
             size.set_y(constraint.max.y());
         }
 
+        if let Some(scroll_state) = self.scroll_state.as_ref() {
+            scroll_state.update(cx, |scroll_state, _| {
+                scroll_state.scroll_position =
+                    scroll_state.scroll_position.min(-remaining_space).max(0.);
+            });
+        }
+
         (size, remaining_space)
     }
 
@@ -181,7 +204,16 @@ impl Element for Flex {
         if overflowing {
             cx.scene.push_layer(Some(bounds));
         }
+
         let mut child_origin = bounds.origin();
+        if let Some(scroll_state) = self.scroll_state.as_ref() {
+            let scroll_position = scroll_state.read(cx).scroll_position;
+            match self.axis {
+                Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position),
+                Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position),
+            }
+        }
+
         for child in &mut self.children {
             if *remaining_space > 0. {
                 if let Some(metadata) = child.metadata::<FlexParentData>() {
@@ -208,8 +240,8 @@ impl Element for Flex {
     fn dispatch_event(
         &mut self,
         event: &Event,
-        _: RectF,
-        _: &mut Self::LayoutState,
+        bounds: RectF,
+        remaining_space: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,
     ) -> bool {
@@ -217,6 +249,41 @@ impl Element for Flex {
         for child in &mut self.children {
             handled = child.dispatch_event(event, cx) || handled;
         }
+        if !handled {
+            if let &Event::ScrollWheel {
+                position,
+                delta,
+                precise,
+            } = event
+            {
+                if *remaining_space < 0. && bounds.contains_point(position) {
+                    if let Some(scroll_state) = self.scroll_state.as_ref() {
+                        scroll_state.update(cx, |scroll_state, cx| {
+                            dbg!(precise, delta);
+
+                            let mut delta = match self.axis {
+                                Axis::Horizontal => {
+                                    if delta.x() != 0. {
+                                        delta.x()
+                                    } else {
+                                        delta.y()
+                                    }
+                                }
+                                Axis::Vertical => delta.y(),
+                            };
+                            if !precise {
+                                delta *= 20.;
+                            }
+
+                            scroll_state.scroll_position -= delta;
+
+                            handled = true;
+                            cx.notify();
+                        });
+                    }
+                }
+            }
+        }
         handled
     }
 

crates/workspace/src/pane.rs 🔗

@@ -633,7 +633,7 @@ impl Pane {
         enum Tabs {}
         let pane = cx.handle();
         let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
-            let mut row = Flex::row();
+            let mut row = Flex::row().scrollable::<Tabs, _>(1, cx);
             for (ix, item) in self.items.iter().enumerate() {
                 let is_active = ix == self.active_item_index;