@@ -1384,6 +1384,10 @@ impl Element for Div {
(child_max - child_min).into()
};
+ if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() {
+ scroll_handle.scroll_to_active_item();
+ }
+
self.interactivity.prepaint(
global_id,
inspector_id,
@@ -2986,8 +2990,7 @@ where
}
/// Represents an element that can be scrolled *to* in its parent element.
-///
-/// Contrary to [ScrollHandle::scroll_to_item], an anchored element does not have to be an immediate child of the parent.
+/// Contrary to [ScrollHandle::scroll_to_active_item], an anchored element does not have to be an immediate child of the parent.
#[derive(Clone)]
pub struct ScrollAnchor {
handle: ScrollHandle,
@@ -3022,6 +3025,7 @@ struct ScrollHandleState {
child_bounds: Vec<Bounds<Pixels>>,
scroll_to_bottom: bool,
overflow: Point<Overflow>,
+ active_item: Option<usize>,
}
/// A handle to the scrollable aspects of an element.
@@ -3081,32 +3085,44 @@ impl ScrollHandle {
self.0.borrow().child_bounds.get(ix).cloned()
}
- /// scroll_to_item scrolls the minimal amount to ensure that the child is
- /// fully visible
+ /// Update [ScrollHandleState]'s active item for scrolling to in prepaint
pub fn scroll_to_item(&self, ix: usize) {
- let state = self.0.borrow();
+ let mut state = self.0.borrow_mut();
+ state.active_item = Some(ix);
+ }
+
+ /// Scrolls the minimal amount to ensure that the child is
+ /// fully visible
+ fn scroll_to_active_item(&self) {
+ let mut state = self.0.borrow_mut();
- let Some(bounds) = state.child_bounds.get(ix) else {
+ let Some(active_item_index) = state.active_item else {
return;
};
+ let active_item = match state.child_bounds.get(active_item_index) {
+ Some(bounds) => {
+ 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();
+ }
+ }
- 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 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();
+ 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();
+ }
+ }
+ None
}
- }
+ None => Some(active_item_index),
+ };
+ state.active_item = active_item;
}
/// Scrolls to the bottom.
@@ -367,6 +367,9 @@ pub struct Pane {
max_tabs: Option<NonZeroUsize>,
_subscriptions: Vec<Subscription>,
tab_bar_scroll_handle: ScrollHandle,
+ /// This is set to true if a user scroll has occurred more recently than a system scroll
+ /// We want to suppress certain system scrolls when the user has intentionally scrolled
+ suppress_scroll: bool,
/// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
/// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
display_nav_history_buttons: Option<bool>,
@@ -497,6 +500,7 @@ impl Pane {
}))),
toolbar: cx.new(|_| Toolbar::new()),
tab_bar_scroll_handle: ScrollHandle::new(),
+ suppress_scroll: false,
drag_split_direction: None,
workspace,
project: project.downgrade(),
@@ -573,6 +577,9 @@ impl Pane {
if !self.was_focused {
self.was_focused = true;
self.update_history(self.active_item_index);
+ if !self.suppress_scroll && self.items.get(self.active_item_index).is_some() {
+ self.update_active_tab(self.active_item_index);
+ }
cx.emit(Event::Focus);
cx.notify();
}
@@ -618,6 +625,7 @@ impl Pane {
self.toolbar.update(cx, |toolbar, cx| {
toolbar.focus_changed(false, window, cx);
});
+
cx.notify();
}
@@ -1124,6 +1132,7 @@ impl Pane {
}
} else {
self.items.insert(insertion_index, item.clone());
+ cx.notify();
if activate {
if insertion_index <= self.active_item_index
@@ -1134,7 +1143,6 @@ impl Pane {
self.activate_item(insertion_index, activate_pane, focus_item, window, cx);
}
- cx.notify();
}
cx.emit(Event::AddItem { item });
@@ -1272,15 +1280,18 @@ impl Pane {
focus_changed: focus_item,
});
- if !self.is_tab_pinned(index) {
- self.tab_bar_scroll_handle
- .scroll_to_item(index - self.pinned_tab_count);
- }
-
+ self.update_active_tab(index);
cx.notify();
}
}
+ fn update_active_tab(&mut self, index: usize) {
+ if !self.is_tab_pinned(index) {
+ self.suppress_scroll = false;
+ self.tab_bar_scroll_handle.scroll_to_item(index);
+ }
+ }
+
fn update_history(&mut self, index: usize) {
if let Some(newly_active_item) = self.items.get(index) {
self.activation_history
@@ -3028,6 +3039,9 @@ impl Pane {
.overflow_x_scroll()
.w_full()
.track_scroll(&self.tab_bar_scroll_handle)
+ .on_scroll_wheel(cx.listener(|this, _, _, _| {
+ this.suppress_scroll = true;
+ }))
.children(unpinned_tabs)
.child(
div()
@@ -4095,7 +4109,7 @@ mod tests {
use super::*;
use crate::item::test::{TestItem, TestProjectItem};
- use gpui::{TestAppContext, VisualTestContext};
+ use gpui::{TestAppContext, VisualTestContext, size};
use project::FakeFs;
use settings::SettingsStore;
use theme::LoadThemes;
@@ -6310,6 +6324,42 @@ mod tests {
});
}
+ #[gpui::test]
+ async fn test_new_tab_scrolls_into_view_completely(cx: &mut TestAppContext) {
+ // Arrange
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+
+ let project = Project::test(fs, None, cx).await;
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
+ let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+ cx.simulate_resize(size(px(300.), px(300.)));
+
+ add_labeled_item(&pane, "untitled", false, cx);
+ add_labeled_item(&pane, "untitled", false, cx);
+ add_labeled_item(&pane, "untitled", false, cx);
+ add_labeled_item(&pane, "untitled", false, cx);
+ // Act: this should trigger a scroll
+ add_labeled_item(&pane, "untitled", false, cx);
+ // Assert
+ let tab_bar_scroll_handle =
+ pane.update_in(cx, |pane, _window, _cx| pane.tab_bar_scroll_handle.clone());
+ assert_eq!(tab_bar_scroll_handle.children_count(), 6);
+ let tab_bounds = cx.debug_bounds("TAB-3").unwrap();
+ let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
+ let scroll_bounds = tab_bar_scroll_handle.bounds();
+ let scroll_offset = tab_bar_scroll_handle.offset();
+ assert!(tab_bounds.right() <= scroll_bounds.right() + scroll_offset.x);
+ // -39.75 is the magic number for this setup
+ assert_eq!(scroll_offset.x, px(-39.75));
+ assert!(
+ !tab_bounds.intersects(&new_tab_button_bounds),
+ "Tab should not overlap with the new tab button, if this is failing check if there's been a redesign!"
+ );
+ }
+
#[gpui::test]
async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
init_test(cx);