Scroll the tab bar to show the active tab

Max Brunsfeld , Nathan , and Marshall created

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Marshall <marshall@zed.dev>

Change summary

crates/gpui2/src/elements/div.rs     | 25 ++++++++++++++++++++-----
crates/ui2/src/components/tab_bar.rs | 27 +++++++++++++++------------
crates/workspace2/src/pane.rs        | 23 ++++++++++++++---------
3 files changed, 49 insertions(+), 26 deletions(-)

Detailed changes

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

@@ -1016,6 +1016,10 @@ impl Interactivity {
 
         let overflow = style.overflow;
         if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll {
+            if let Some(scroll_handle) = &self.scroll_handle {
+                scroll_handle.0.borrow_mut().overflow = overflow;
+            }
+
             let scroll_offset = element_state
                 .scroll_offset
                 .get_or_insert_with(Rc::default)
@@ -1420,6 +1424,7 @@ struct ScrollHandleState {
     bounds: Bounds<Pixels>,
     child_bounds: Vec<Bounds<Pixels>>,
     requested_scroll_top: Option<(usize, Pixels)>,
+    overflow: Point<Overflow>,
 }
 
 #[derive(Clone)]
@@ -1465,12 +1470,22 @@ impl ScrollHandle {
             return;
         };
 
-        let scroll_offset = state.offset.borrow().y;
+        let mut scroll_offset = state.offset.borrow_mut();
+
+        if state.overflow.y == Overflow::Scroll {
+            if bounds.top() + scroll_offset.y < state.bounds.top() {
+                scroll_offset.y = state.bounds.top() - bounds.top();
+            } else if bounds.bottom() + scroll_offset.y > state.bounds.bottom() {
+                scroll_offset.y = state.bounds.bottom() - bounds.bottom();
+            }
+        }
 
-        if bounds.top() + scroll_offset < state.bounds.top() {
-            state.offset.borrow_mut().y = state.bounds.top() - bounds.top();
-        } else if bounds.bottom() + scroll_offset > state.bounds.bottom() {
-            state.offset.borrow_mut().y = state.bounds.bottom() - bounds.bottom();
+        if state.overflow.x == Overflow::Scroll {
+            if bounds.left() + scroll_offset.x < state.bounds.left() {
+                scroll_offset.x = state.bounds.left() - bounds.left();
+            } else if bounds.right() + scroll_offset.x > state.bounds.right() {
+                scroll_offset.x = state.bounds.right() - bounds.right();
+            }
         }
     }
 

crates/ui2/src/components/tab_bar.rs 🔗

@@ -1,26 +1,33 @@
-use gpui::{AnyElement, Stateful};
+use gpui::{AnyElement, ScrollHandle, Stateful};
 use smallvec::SmallVec;
 
 use crate::prelude::*;
 
 #[derive(IntoElement)]
 pub struct TabBar {
-    div: Stateful<Div>,
+    id: ElementId,
     start_children: SmallVec<[AnyElement; 2]>,
     children: SmallVec<[AnyElement; 2]>,
     end_children: SmallVec<[AnyElement; 2]>,
+    scroll_handle: Option<ScrollHandle>,
 }
 
 impl TabBar {
     pub fn new(id: impl Into<ElementId>) -> Self {
         Self {
-            div: div().id(id),
+            id: id.into(),
             start_children: SmallVec::new(),
             children: SmallVec::new(),
             end_children: SmallVec::new(),
+            scroll_handle: None,
         }
     }
 
+    pub fn track_scroll(mut self, scroll_handle: ScrollHandle) -> Self {
+        self.scroll_handle = Some(scroll_handle);
+        self
+    }
+
     pub fn start_children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
         &mut self.start_children
     }
@@ -81,21 +88,14 @@ impl ParentElement for TabBar {
     }
 }
 
-impl InteractiveElement for TabBar {
-    fn interactivity(&mut self) -> &mut gpui::Interactivity {
-        self.div.interactivity()
-    }
-}
-
-impl StatefulInteractiveElement for TabBar {}
-
 impl RenderOnce for TabBar {
     type Rendered = Stateful<Div>;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
         const HEIGHT_IN_REMS: f32 = 30. / 16.;
 
-        self.div
+        div()
+            .id(self.id)
             .group("tab_bar")
             .flex()
             .flex_none()
@@ -134,6 +134,9 @@ impl RenderOnce for TabBar {
                             .z_index(2)
                             .flex_grow()
                             .overflow_x_scroll()
+                            .when_some(self.scroll_handle, |cx, scroll_handle| {
+                                cx.track_scroll(&scroll_handle)
+                            })
                             .children(self.children),
                     ),
             )

crates/workspace2/src/pane.rs 🔗

@@ -10,7 +10,7 @@ use gpui::{
     actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyWeakView, AppContext,
     AsyncWindowContext, DismissEvent, Div, EntityId, EventEmitter, FocusHandle, Focusable,
     FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, PromptLevel, Render,
-    Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
+    ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use parking_lot::Mutex;
 use project::{Project, ProjectEntryId, ProjectPath};
@@ -177,10 +177,8 @@ pub struct Pane {
     was_focused: bool,
     active_item_index: usize,
     last_focused_view_by_item: HashMap<EntityId, FocusHandle>,
-    autoscroll: bool,
     nav_history: NavHistory,
     toolbar: View<Toolbar>,
-    tab_bar_focus_handle: FocusHandle,
     new_item_menu: Option<View<ContextMenu>>,
     split_item_menu: Option<View<ContextMenu>>,
     //     tab_context_menu: ViewHandle<ContextMenu>,
@@ -190,6 +188,7 @@ pub struct Pane {
     can_split: bool,
     //     render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
     subscriptions: Vec<Subscription>,
+    tab_bar_scroll_handle: ScrollHandle,
 }
 
 pub struct ItemNavHistory {
@@ -353,7 +352,6 @@ impl Pane {
             zoomed: false,
             active_item_index: 0,
             last_focused_view_by_item: Default::default(),
-            autoscroll: false,
             nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
                 mode: NavigationMode::Normal,
                 backward_stack: Default::default(),
@@ -364,9 +362,9 @@ impl Pane {
                 next_timestamp,
             }))),
             toolbar: cx.build_view(|_| Toolbar::new()),
-            tab_bar_focus_handle: cx.focus_handle(),
             new_item_menu: None,
             split_item_menu: None,
+            tab_bar_scroll_handle: ScrollHandle::new(),
             // tab_bar_context_menu: TabBarContextMenu {
             //     kind: TabBarContextMenuKind::New,
             //     handle: context_menu,
@@ -469,8 +467,8 @@ impl Pane {
                 }
 
                 active_item.focus_handle(cx).focus(cx);
-            } else if !self.tab_bar_focus_handle.contains_focused(cx) {
-                if let Some(focused) = cx.focused() {
+            } else if let Some(focused) = cx.focused() {
+                if !self.context_menu_focused(cx) {
                     self.last_focused_view_by_item
                         .insert(active_item.item_id(), focused);
                 }
@@ -478,6 +476,13 @@ impl Pane {
         }
     }
 
+    fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
+        self.new_item_menu
+            .as_ref()
+            .or(self.split_item_menu.as_ref())
+            .map_or(false, |menu| menu.focus_handle(cx).is_focused(cx))
+    }
+
     fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
         self.was_focused = false;
         self.toolbar.update(cx, |toolbar, cx| {
@@ -794,7 +799,7 @@ impl Pane {
                 self.focus_active_item(cx);
             }
 
-            self.autoscroll = true;
+            self.tab_bar_scroll_handle.scroll_to_item(index);
             cx.notify();
         }
     }
@@ -1584,6 +1589,7 @@ impl Pane {
 
     fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
         TabBar::new("tab_bar")
+            .track_scroll(self.tab_bar_scroll_handle.clone())
             .start_child(
                 IconButton::new("navigate_backward", Icon::ArrowLeft)
                     .icon_size(IconSize::Small)
@@ -1669,7 +1675,6 @@ impl Pane {
                         }),
                     ),
             )
-            .track_focus(&self.tab_bar_focus_handle)
     }
 
     fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {