mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Pane tabs: Scroll entire new tab into view (#36827)
The state of the child bounds is not up-to-date when `scroll_to_item` gets triggered, causing the new tab to not scroll completely into view. Closes #36317 Release Notes: - Fix an issue where a new tab is only partially visible on creation.
This commit is contained in:
parent
624e448492
commit
49335d54be
3 changed files with 94 additions and 34 deletions
|
|
@ -6514,14 +6514,8 @@ async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
|
|||
cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
|
||||
cx.update(|window, _cx| window.refresh());
|
||||
|
||||
let tab_bounds = cx.debug_bounds("TAB-2").unwrap();
|
||||
let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
|
||||
|
||||
assert!(
|
||||
tab_bounds.intersects(&new_tab_button_bounds),
|
||||
"Tab should overlap with the new tab button, if this is failing check if there's been a redesign!"
|
||||
);
|
||||
|
||||
cx.simulate_event(MouseDownEvent {
|
||||
button: MouseButton::Right,
|
||||
position: new_tab_button_bounds.center(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
let Some(bounds) = state.child_bounds.get(ix) else {
|
||||
/// 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(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();
|
||||
|
||||
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.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();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
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 => 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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue