Merge pull request #745 from zed-industries/scroll-tabs

Antonio Scandurra created

Allow pane tabs to be scrolled when they overflow

Change summary

crates/editor/src/display_map.rs         |   5 -
crates/gpui/src/elements/flex.rs         | 104 +++++++++++++++++++++++++-
crates/gpui/src/elements/uniform_list.rs |  15 +--
crates/gpui/src/platform/event.rs        |  17 ++++
crates/workspace/src/pane.rs             |  40 +++++----
5 files changed, 146 insertions(+), 35 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -1165,10 +1165,7 @@ pub mod tests {
                         *markers[0].column_mut() += 1;
                     }
 
-                    assert_eq!(
-                        unmarked_snapshot.clip_point(dbg!(markers[0]), bias),
-                        markers[1]
-                    )
+                    assert_eq!(unmarked_snapshot.clip_point(markers[0], bias), markers[1])
                 }
             };
         }

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,16 @@ use pathfinder_geometry::{
 };
 use serde_json::json;
 
+#[derive(Default)]
+struct ScrollState {
+    scroll_to: Option<usize>,
+    scroll_position: f32,
+}
+
 pub struct Flex {
     axis: Axis,
     children: Vec<ElementBox>,
+    scroll_state: Option<ElementStateHandle<ScrollState>>,
 }
 
 impl Flex {
@@ -21,6 +28,7 @@ impl Flex {
         Self {
             axis,
             children: Default::default(),
+            scroll_state: None,
         }
     }
 
@@ -32,6 +40,22 @@ impl Flex {
         Self::new(Axis::Vertical)
     }
 
+    pub fn scrollable<Tag, C>(
+        mut self,
+        element_id: usize,
+        scroll_to: Option<usize>,
+        cx: &mut C,
+    ) -> Self
+    where
+        Tag: 'static,
+        C: ElementStateContext,
+    {
+        let scroll_state = cx.element_state::<Tag, ScrollState>(element_id);
+        scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to);
+        self.scroll_state = Some(scroll_state);
+        self
+    }
+
     fn layout_flex_children(
         &mut self,
         layout_expanded: bool,
@@ -167,6 +191,30 @@ 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, _| {
+                if let Some(scroll_to) = scroll_state.scroll_to.take() {
+                    let visible_start = scroll_state.scroll_position;
+                    let visible_end = visible_start + size.along(self.axis);
+                    if let Some(child) = self.children.get(scroll_to) {
+                        let child_start: f32 = self.children[..scroll_to]
+                            .iter()
+                            .map(|c| c.size().along(self.axis))
+                            .sum();
+                        let child_end = child_start + child.size().along(self.axis);
+                        if child_start < visible_start {
+                            scroll_state.scroll_position = child_start;
+                        } else if child_end > visible_end {
+                            scroll_state.scroll_position = child_end - size.along(self.axis);
+                        }
+                    }
+                }
+
+                scroll_state.scroll_position =
+                    scroll_state.scroll_position.min(-remaining_space).max(0.);
+            });
+        }
+
         (size, remaining_space)
     }
 
@@ -181,7 +229,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,15 +265,54 @@ 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 {
+        if let Some(position) = event.position() {
+            if !bounds.contains_point(position) {
+                return false;
+            }
+        }
+
         let mut handled = false;
         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| {
+                            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/gpui/src/elements/uniform_list.rs 🔗

@@ -8,11 +8,10 @@ use crate::{
     ElementBox,
 };
 use json::ToJson;
-use parking_lot::Mutex;
-use std::{cmp, ops::Range, sync::Arc};
+use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
 
 #[derive(Clone, Default)]
-pub struct UniformListState(Arc<Mutex<StateInner>>);
+pub struct UniformListState(Rc<RefCell<StateInner>>);
 
 #[derive(Debug)]
 pub enum ScrollTarget {
@@ -22,11 +21,11 @@ pub enum ScrollTarget {
 
 impl UniformListState {
     pub fn scroll_to(&self, scroll_to: ScrollTarget) {
-        self.0.lock().scroll_to = Some(scroll_to);
+        self.0.borrow_mut().scroll_to = Some(scroll_to);
     }
 
     pub fn scroll_top(&self) -> f32 {
-        self.0.lock().scroll_top
+        self.0.borrow().scroll_top
     }
 }
 
@@ -96,7 +95,7 @@ where
             delta *= 20.;
         }
 
-        let mut state = self.state.0.lock();
+        let mut state = self.state.0.borrow_mut();
         state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
         cx.notify();
 
@@ -104,7 +103,7 @@ where
     }
 
     fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) {
-        let mut state = self.state.0.lock();
+        let mut state = self.state.0.borrow_mut();
 
         if let Some(scroll_to) = state.scroll_to.take() {
             let item_ix;
@@ -141,7 +140,7 @@ where
     }
 
     fn scroll_top(&self) -> f32 {
-        self.state.0.lock().scroll_top
+        self.state.0.borrow().scroll_top
     }
 }
 

crates/gpui/src/platform/event.rs 🔗

@@ -61,3 +61,20 @@ pub enum Event {
         left_mouse_down: bool,
     },
 }
+
+impl Event {
+    pub fn position(&self) -> Option<Vector2F> {
+        match self {
+            Event::KeyDown { .. } => None,
+            Event::ScrollWheel { position, .. }
+            | Event::LeftMouseDown { position, .. }
+            | Event::LeftMouseUp { position }
+            | Event::LeftMouseDragged { position }
+            | Event::RightMouseDown { position, .. }
+            | Event::RightMouseUp { position }
+            | Event::NavigateMouseDown { position, .. }
+            | Event::NavigateMouseUp { position, .. }
+            | Event::MouseMoved { position, .. } => Some(*position),
+        }
+    }
+}

crates/workspace/src/pane.rs 🔗

@@ -101,6 +101,7 @@ pub enum Event {
 pub struct Pane {
     items: Vec<Box<dyn ItemHandle>>,
     active_item_index: usize,
+    autoscroll: bool,
     nav_history: Rc<RefCell<NavHistory>>,
     toolbar: ViewHandle<Toolbar>,
 }
@@ -142,6 +143,7 @@ impl Pane {
         Self {
             items: Vec::new(),
             active_item_index: 0,
+            autoscroll: false,
             nav_history: Default::default(),
             toolbar: cx.add_view(|_| Toolbar::new()),
         }
@@ -200,27 +202,19 @@ impl Pane {
                     .upgrade(cx)
                     .and_then(|v| pane.index_for_item(v.as_ref()))
                 {
-                    if let Some(item) = pane.active_item() {
-                        pane.nav_history.borrow_mut().set_mode(mode);
-                        item.deactivated(cx);
-                        pane.nav_history
-                            .borrow_mut()
-                            .set_mode(NavigationMode::Normal);
-                    }
-
-                    let prev_active_index = mem::replace(&mut pane.active_item_index, index);
+                    let prev_active_item_index = pane.active_item_index;
+                    pane.nav_history.borrow_mut().set_mode(mode);
+                    pane.activate_item(index, true, cx);
+                    pane.nav_history
+                        .borrow_mut()
+                        .set_mode(NavigationMode::Normal);
 
-                    let mut navigated = prev_active_index != pane.active_item_index;
+                    let mut navigated = prev_active_item_index != pane.active_item_index;
                     if let Some(data) = entry.data {
                         navigated |= pane.active_item()?.navigate(data, cx);
                     }
 
                     if navigated {
-                        pane.focus_active_item(cx);
-                        pane.update_toolbar(cx);
-                        pane.activate(cx);
-                        cx.emit(Event::ActivateItem { local: true });
-                        cx.notify();
                         break None;
                     }
                 }
@@ -376,10 +370,12 @@ impl Pane {
     }
 
     pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext<Self>) {
+        use NavigationMode::{GoingBack, GoingForward};
         if index < self.items.len() {
             let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
-            if prev_active_item_ix != self.active_item_index
-                && prev_active_item_ix < self.items.len()
+            if matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
+                || (prev_active_item_ix != self.active_item_index
+                    && prev_active_item_ix < self.items.len())
             {
                 self.items[prev_active_item_ix].deactivated(cx);
                 cx.emit(Event::ActivateItem { local });
@@ -389,6 +385,7 @@ impl Pane {
                 self.focus_active_item(cx);
                 self.activate(cx);
             }
+            self.autoscroll = true;
             cx.notify();
         }
     }
@@ -628,13 +625,18 @@ impl Pane {
         });
     }
 
-    fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
+    fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         let theme = cx.global::<Settings>().theme.clone();
 
         enum Tabs {}
         let pane = cx.handle();
         let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
-            let mut row = Flex::row();
+            let autoscroll = if mem::take(&mut self.autoscroll) {
+                Some(self.active_item_index)
+            } else {
+                None
+            };
+            let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
             for (ix, item) in self.items.iter().enumerate() {
                 let is_active = ix == self.active_item_index;