Autoscroll to active tab when activating a new item

Antonio Scandurra created

Change summary

crates/gpui/src/elements/flex.rs | 29 +++++++++++++++++++++++++++--
crates/workspace/src/pane.rs     | 12 ++++++++++--
2 files changed, 37 insertions(+), 4 deletions(-)

Detailed changes

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

@@ -13,6 +13,7 @@ use serde_json::json;
 
 #[derive(Default)]
 struct ScrollState {
+    scroll_to: Option<usize>,
     scroll_position: f32,
 }
 
@@ -39,12 +40,19 @@ impl Flex {
         Self::new(Axis::Vertical)
     }
 
-    pub fn scrollable<Tag, C>(mut self, element_id: usize, cx: &mut C) -> Self
+    pub fn scrollable<Tag, C>(
+        mut self,
+        element_id: usize,
+        scroll_to: Option<usize>,
+        cx: &mut C,
+    ) -> Self
     where
         Tag: 'static,
         C: ElementStateContext,
     {
-        self.scroll_state = Some(cx.element_state::<Tag, ScrollState>(element_id));
+        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
     }
 
@@ -185,6 +193,23 @@ impl Element for Flex {
 
         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.);
             });

crates/workspace/src/pane.rs 🔗

@@ -100,6 +100,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>,
 }
@@ -141,6 +142,7 @@ impl Pane {
         Self {
             items: Vec::new(),
             active_item_index: 0,
+            autoscroll: false,
             nav_history: Default::default(),
             toolbar: cx.add_view(|_| Toolbar::new()),
         }
@@ -388,6 +390,7 @@ impl Pane {
                 self.focus_active_item(cx);
                 self.activate(cx);
             }
+            self.autoscroll = true;
             cx.notify();
         }
     }
@@ -627,13 +630,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().scrollable::<Tabs, _>(1, cx);
+            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;